Python带你从浅入深探究Tuple(基础篇)


Posted in Python onMay 15, 2021

元组

Python中的元组容器序列(tuple)与列表容器序列(list)具有极大的相似之处,因此也常被称为不可变的列表。

但是两者之间也有很多的差距,元组侧重于数据的展示,而列表侧重于数据的存储与操作。

它们非常相似,虽然都可以存储任意类型的数据,但是一个元组定义好之后就不能够再进行修改。

元组特性

元组的特点:

  • 元组属于容器序列
  • 元组属于不可变类型
  • 元组底层由顺序存储组成,而顺序存储是线性结构的一种

基本声明

以下是使用类实例化的形式进行对象声明:

tup = tuple((1, 2, 3, 4, 5))
print("值:%r,类型:%r" % (tup, type(tup)))

# 值:(1, 2, 3, 4, 5),类型:<class 'tuple'>

也可以选择使用更方便的字面量形式进行对象声明,使用逗号对数据项之间进行分割:

tup = 1, 2, 3, 4, 5
print("值:%r,类型:%r" % (tup, type(tup)))

# 值:(1, 2, 3, 4, 5),类型:<class 'tuple'>

为了美观,我们一般会在两侧加上(),但是要确定一点,元组定义是逗号分隔的数据项,而并非是()包裹的数据项:

tup = (1, 2, 3, 4, 5)
print("值:%r,类型:%r" % (tup, type(tup)))

# 值:(1, 2, 3, 4, 5),类型:<class 'tuple'>

多维元组

当一个元组中嵌套另一个元组,该元组就可以称为多维元组。

如下,定义一个2维元组:

tup = (1, 2, 3, 4, 5)
print("值:%r,类型:%r" % (tup, type(tup)))

# 值:(1, 2, 3, 4, 5),类型:<class 'tuple'>

续行操作

在Python中,元组中的数据项如果过多,可能会导致整个元组太长,太长的元组是不符合PEP8规范的。

每行最大的字符数不可超过79,文档字符或者注释每行不可超过72

Python虽然提供了续行符\,但是在元组中可以忽略续行符,如下所示:

tup = (1, 2, ("三", "四"))
print("值:%r,类型:%r" % (tup, type(tup)))

# 值:(1, 2, ('三', '四')),类型:<class 'tuple'>

类型转换

元组支持与布尔型、字符串、列表、以及集合类型进行类型转换:

tup = (1, 2, 3)
bTup = bool(tup)    # 布尔类型
strTup = str(tup)   # 字符串类型
liTup = list(tup)   # 列表类型
setTup = set(tup)   # 集合类型

print("值:%r,类型:%r" % (bTup, type(bTup)))
print("值:%r,类型:%r" % (strTup, type(strTup)))
print("值:%r,类型:%r" % (liTup, type(liTup)))
print("值:%r,类型:%r" % (setTup, type(setTup)))

# 值:True,类型:<class 'bool'>
# 值:'(1, 2, 3)',类型:<class 'str'>
# 值:[1, 2, 3],类型:<class 'list'>
# 值:{1, 2, 3},类型:<class 'set'>

如果一个2维元组遵循一定的规律,那么也可以将其转换为字典类型:

tup = (("k1", "v1"), ("k2", "v2"), ("k3", "v3"))
dictTuple = dict(tup)

print("值:%r,类型:%r" % (dictTuple, type(dictTuple)))

# 值:{'k1': 'v1', 'k2': 'v2', 'k3': 'v3'},类型:<class 'dict'>

索引操作

元组的索引操作仅支持获取数据项。

其他的任意索引操作均不被支持。

使用方法参照列表的索引切片一节。

绝对引用

元组拥有绝对引用的特性,无论是深拷贝还是浅拷贝,都不会获得其副本,而是直接对源对象进行引用。

但是列表没有绝对引用的特性,代码验证如下:

>>> import copy
>>> # 列表的深浅拷贝均创建新列表...
>>> oldLi = [1, 2, 3]
>>> id(oldLi)
4542649096
>>> li1 = copy.copy(oldLi)
>>> id(li1)
4542648840
>>> li2 = copy.deepcopy(oldLi)
>>> id(li2)
4542651208
>>> # 元组的深浅拷贝始终引用老元组
>>> oldTup = (1, 2, 3)
>>> id(oldTup)
4542652920
>>> tup1 = copy.copy(oldTup)
>>> id(tup1)
4542652920
>>> tup2 = copy.deepcopy(oldTup)
>>> id(tup2)
4542652920

Python为何要这样设计?其实仔细想想不难发现,元组不能对其进行操作,仅能获取数据项。

那么也就没有生成多个副本提供给开发人员操作的必要了,因为你修改不了元组,索性直接使用绝对引用策略。

值得注意的一点:[:]也是浅拷贝,故对元组来说属于绝对引用范畴。

元组的陷阱

Leonardo Rochael在2013年的Python巴西会议提出了一个非常具有思考意义的问题。

我们先来看一下:

>>> t = (1, 2, [30, 40])
>>> t[-1] += [50, 60]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

现在,t到底会发生下面4种情况中的哪一种?

  1. t 变成 (1, 2, [30, 40, 50, 60])。
  2. 因为 tuple 不支持对它的数据项赋值,所以会抛出 TypeError 异常。
  3. 以上两个都不是。a 和 b 都是对的。

正确答案是4,t确实会变成 (1, 2, [30, 40, 50, 60]),但同时元组是不可变类型故会引发TypeError异常的出现。

>>> t
(1, 2, [30, 40, 50, 60])

如果是使用extend()对t[-1]的列表进行数据项的增加,则答案会变成1。

我当初在看了这个问题后,暗自告诉自己了2件事情:

  • list的数据项增加尽量不要使用+=,而应该使用append()或者extend()

Ps:我也不知道自己为什么会产生这样的想法,但这个想法确实伴随我很长时间,直至现在

  • tuple中不要存放可变类型的数据,如list、set、dict等..

元组更多的作用是展示数据,而不是操作数据。

举个例子,当用户根据某个操作获取到了众多数据项之后,你可以将这些数据项做出元组并返回。

用户对被返回的原对象只能看,不能修改,若想修改则必须创建新其他类型对象。

解构方法

元组的解构方法与列表使用相同。

使用方法参照列表的解构方法一节。

常用方法

方法一览

常用的list方法一览表:

方法名 返回值 描述
count() integer 返回数据项在T中出现的次数
index() integer 返回第一个数据项在T中出现位置的索引,若值不存在,则抛出ValueError

基础公用函数:

函数名 返回值 描述
len() integer 返回容器中的项目数
enumerate() iterator for index, value of iterable 返回一个可迭代对象,其中以小元组的形式包裹数据项与正向索引的对应关系
reversed() ... 详情参见函数章节
sorted() ... 详情参见函数章节

点我跳转

源码一览:点我跳转

以下是截取了一些关键性源代码,并且做上了中文注释,方便查阅。

每一个元组都有几个关键性的属性:

Py_ssize_t ob_refcnt;     // 引用计数器
Py_ssize_t ob_size;       // 数据项个数,即元组大小
PyObject *ob_item[1];     // 存储元组中的数据项 [指针, ]

关于缓存free_list的属性:

PyTuple_MAXSAVESIZE     // 相当于图中的 free_num ,最大20,即纵向扩展的缓存元组长度
PyTuple_MAXFREELIST     // 图中 free_list 的横向扩展缓存列表个数,最大2000

创建元组

空元组

PyObject *
PyTuple_New(Py_ssize_t size)
{
    PyTupleObject *op;
    // 缓存相关
    Py_ssize_t i;
    
    // 元组的大小不能小于0
    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }
#if PyTuple_MAXSAVESIZE > 0

    // 创建空元组,优先从缓存中获取
    // size = 0 表示这是一个空元组,从free_list[0]中获取空元组
    if (size == 0 && free_list[0]) {
        // op就是空元组
        op = free_list[0];
        // 新增空元组引用计数器 + 1
        Py_INCREF(op);
#ifdef COUNT_ALLOCS
        tuple_zero_allocs++;
#endif
        // 返回空元组的指针
        return (PyObject *) op;
    }
    
    // 如果创建的不是空元组,且这个创建的元组数据项个数小于20,并且free_list[size]不等于空,表示有缓存
    // 则从缓存中去获取,不再重新开辟内存
    if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
        // 拿出元组
        free_list[size] = (PyTupleObject *) op->ob_item[0];
        // num_free减1
        numfree[size]--;
#ifdef COUNT_ALLOCS
        fast_tuple_allocs++;
#endif
        /* Inline PyObject_InitVar */
        // 初始化,定义这个元组的长度为数据项个数
#ifdef Py_TRACE_REFS
        Py_SIZE(op) = size;
        // 定义类型为 tuple
        Py_TYPE(op) = &PyTuple_Type;
#endif
        // 增加一次新的引用
        _Py_NewReference((PyObject *)op);
    }
    
    // 如果是空元组
    else
#endif
    {
        // 检查内存情况,是否充足
        /* Check for overflow */
        if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - sizeof(PyTupleObject) -
                    sizeof(PyObject *)) / sizeof(PyObject *)) {
            return PyErr_NoMemory();
        }
        // 开辟内存,并获得一个元组:op
        op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
        if (op == NULL)
            return NULL;
    }
    
    // 空元组的每一个槽位都是NULL
    for (i=0; i < size; i++)
        op->ob_item[i] = NULL;
        
#if PyTuple_MAXSAVESIZE > 0
   // 缓存空元组
    if (size == 0) {
        free_list[0] = op;
        ++numfree[0];
        Py_INCREF(op);          /* extra INCREF so that this is never freed */
    }
#endif
#ifdef SHOW_TRACK_COUNT
    count_tracked++;
#endif

    // 将元组加入到GC机制中,用于内存管理
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

可迭代对象转元组

这个不在tupleobject.c源码中,而是在abstract.c源码中。

官网参考:点我跳转

源码一览:点我跳转

PyObject *
PySequence_Tuple(PyObject *v)
{
    PyObject *it;  /* iter(v) */
    Py_ssize_t n;             /* guess for result tuple size */
    PyObject *result = NULL;
    Py_ssize_t j;

    if (v == NULL) {
        return null_error();
    }

    /* Special-case the common tuple and list cases, for efficiency. */
    // 如果是元组转换元组,如 tup = (1, 2, 3) 或者 tup = ((1, 2, 3))直接返回内存地址
    if (PyTuple_CheckExact(v)) {
        Py_INCREF(v);
        return v;
    }
    
    // 如果是列表转换元组,则执行PyList_AsTuple(),将列表转换为元组
    // 如 tup = ([1, 2, 3])
    if (PyList_CheckExact(v))
        return PyList_AsTuple(v);

    /* Get iterator. */
    // 获取迭代器, tup = (range(1, 4).__iter__())
 
    it = PyObject_GetIter(v);
    if (it == NULL)
        return NULL;

    /* Guess result size and allocate space. */
    // 猜想迭代器长度,也就是猜一下有多少个数据项
    n = PyObject_LengthHint(v, 10);
    if (n == -1)
        goto Fail;
        
    // 根据猜想的迭代器长度,进行元组的内存开辟
    result = PyTuple_New(n);
    if (result == NULL)
        goto Fail;

    /* Fill the tuple. */
    // 将迭代器中每个数据项添加至元组中
    for (j = 0; ; ++j) {
        PyObject *item = PyIter_Next(it);
        if (item == NULL) {
            if (PyErr_Occurred())
                goto Fail;
            break;
        }
        
        //如果迭代器中数据项比猜想的多,则证明开辟内存不足需要需要进行扩容
        if (j >= n) {
            size_t newn = (size_t)n;
            /* The over-allocation strategy can grow a bit faster
               than for lists because unlike lists the
               over-allocation isn't permanent -- we reclaim
               the excess before the end of this routine.
               So, grow by ten and then add 25%.
            */
            
            // 假如猜想的是9
            // 第一步:+ 10 
            // 第二步:+ (原长度+10) * 0.25
            // 其实,就是增加【原长度*0.25 + 2.5】
            
            newn += 10u;
            newn += newn >> 2;
            
            // 判断是否超过了元组的数据项个数限制(sys.maxsize)
            if (newn > PY_SSIZE_T_MAX) {
                /* Check for overflow */
                PyErr_NoMemory();
                Py_DECREF(item);
                goto Fail;
            }
            n = (Py_ssize_t)newn;
            // 扩容机制
            if (_PyTuple_Resize(&result, n) != 0) {
                Py_DECREF(item);
                goto Fail;
            }
        }
        
        // 将数据项放入元组之中
        PyTuple_SET_ITEM(result, j, item);
    }

    /* Cut tuple back if guess was too large. */
    
    // 如果猜想的数据项太多,而实际上迭代器中的数据量偏少
    // 则需要对该元组进行缩容
    if (j < n &&
        _PyTuple_Resize(&result, j) != 0)
        goto Fail;

    Py_DECREF(it);
    return result;

Fail:
    Py_XDECREF(result);
    Py_DECREF(it);
    return NULL;
}

列表转元组

这个不在tupleobject.c源码中,而是在listobject.c源码中。

官网参考:点我跳转

源码一览:点我跳转

PyObject *
PyList_AsTuple(PyObject *v)
{
    PyObject *w;
    PyObject **p, **q;
    Py_ssize_t n;
    // 例如:tup = ([1, 2, 3])
    
    // 进行列表的验证
    if (v == NULL || !PyList_Check(v)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    
    // 获取大小,即数据项个数
    n = Py_SIZE(v);
    // 开辟内存
    w = PyTuple_New(n);
    
    // 如果是空元组
    if (w == NULL)
        return NULL;
        
    // 执行迁徙操作
    p = ((PyTupleObject *)w)->ob_item;
    q = ((PyListObject *)v)->ob_item;
    
    // 将列表中数据项的引用,也给元组进行引用
    // 这样列表中数据项和元组中的数据项都引用同1个对象
    while (--n >= 0) {
        // 数据项引用计数 + 1
        Py_INCREF(*q);
        *p = *q;
        p++;
        q++;
    }
    
    // 返回元组
    return w;
}

切片取值

PyObject *
PyTuple_GetSlice(PyObject *op, Py_ssize_t i, Py_ssize_t j)
// 切片会触发该方法
{
    // 如果对空元组进行切片,则会抛出异常
    if (op == NULL || !PyTuple_Check(op)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    // 内部的具体实现方法
    return tupleslice((PyTupleObject *)op, i, j);
}

static PyObject *
tupleslice(PyTupleObject *a, Py_ssize_t ilow,
           Py_ssize_t ihigh)
{
    PyTupleObject *np;
    PyObject **src, **dest;
    Py_ssize_t i;
    Py_ssize_t len;
    
    // 计算索引位置
    if (ilow < 0)
        ilow = 0;
    if (ihigh > Py_SIZE(a))
        ihigh = Py_SIZE(a);
    if (ihigh < ilow)
        ihigh = ilow;
        
    // 如果是[:]的操作,则直接返回源元组对象a的指针,即绝对引用
    if (ilow == 0 && ihigh == Py_SIZE(a) && PyTuple_CheckExact(a)) {
        Py_INCREF(a);
        return (PyObject *)a;
    }
    
    // 初始化新的切片对象元组长度
    len = ihigh - ilow;
    
    // 开始切片,创建了一个新元组np
    np = (PyTupleObject *)PyTuple_New(len);
    if (np == NULL)
        return NULL;
    src = a->ob_item + ilow;
    dest = np->ob_item;
    
    // 对源元组中的数据项的引用计数+1
    for (i = 0; i < len; i++) {
        PyObject *v = src[i];
        Py_INCREF(v);
        dest[i] = v;
    }
    
    // 返回切片对象新元组np的引用
    return (PyObject *)np;
}

缓存相关

static void
tupledealloc(PyTupleObject *op)
{
    Py_ssize_t i;
    Py_ssize_t len =  Py_SIZE(op);
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_SAFE_BEGIN(op)
    
    // 如果元组的长度大于0,则不是一个非空元组
    if (len > 0) {
        i = len;
        // 将内部的数据项引用计数都 - 1
        while (--i >= 0)
            Py_XDECREF(op->ob_item[i]);
#if PyTuple_MAXSAVESIZE > 0
        
        // 准备缓存,判断num_free是否小于20,并且单向链表中的已缓存元组个数小于2000
        if (len < PyTuple_MAXSAVESIZE &&
            numfree[len] < PyTuple_MAXFREELIST &&
            Py_TYPE(op) == &PyTuple_Type)
        {
            // 添加至链表头部
            op->ob_item[0] = (PyObject *) free_list[len];
            // 将num_free + 1
            numfree[len]++;
            free_list[len] = op;
            goto done; /* return */
        }
#endif
    }
    // 内存中进行销毁
    Py_TYPE(op)->tp_free((PyObject *)op);
done:
    Py_TRASHCAN_SAFE_END(op)
}

以上就是老Python带你从浅入深探究Tuple的详细内容,更多关于Python Tuple的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
使用Python的Scrapy框架编写web爬虫的简单示例
Apr 17 Python
python实现两个文件合并功能
Apr 01 Python
Python考拉兹猜想输出序列代码实践
Jul 05 Python
python的移位操作实现详解
Aug 21 Python
Python获取统计自己的qq群成员信息的方法
Nov 15 Python
selenium中get_cookies()和add_cookie()的用法详解
Jan 06 Python
pytorch实现线性拟合方式
Jan 15 Python
Python如何执行精确的浮点数运算
Jul 31 Python
Python调用ffmpeg开源视频处理库,批量处理视频
Nov 16 Python
Python爬虫如何破解JS加密的Cookie
Nov 19 Python
python读写数据读写csv文件(pandas用法)
Dec 14 Python
termux中matplotlib无法显示中文问题的解决方法
Jan 11 Python
Python中zipfile压缩包模块的使用
python 制作一个gui界面的翻译工具
pyqt5打包成exe可执行文件的方法
Python 机器学习工具包SKlearn的安装与使用
python process模块的使用简介
May 14 #Python
django学习之ajax post传参的2种格式实例
May 14 #Python
Python djanjo之csrf防跨站攻击实验过程
You might like
分享自定义的几个PHP功能函数
2015/04/15 PHP
Yii2实现跨mysql数据库关联查询排序功能代码
2017/02/10 PHP
基于jquery的cookie的用法
2011/01/10 Javascript
js 判断浏览器使用的语言示例代码
2014/03/22 Javascript
javascript实例--教你实现扑克牌洗牌功能
2014/05/15 Javascript
jQuery过滤选择器用法分析
2015/02/10 Javascript
JavaScript实现广告的关闭与显示效果实例
2015/07/02 Javascript
pace.js页面加载进度条插件
2015/09/29 Javascript
js获取iframe中的window对象的实现方法
2016/05/20 Javascript
js基于setTimeout与setInterval实现多线程
2016/06/17 Javascript
jQuery.ajax 跨域请求webapi设置headers的解决方案
2016/08/08 Javascript
完美解决jQuery符号$与其他javascript 库、框架冲突的问题
2016/08/09 Javascript
Vue.js实现按钮的动态绑定效果及实现代码
2017/08/21 Javascript
解决vue2.0路由跳转未匹配相应用路由避免出现空白页面的问题
2018/08/24 Javascript
解决百度Echarts图表坐标轴越界的方法
2018/10/17 Javascript
python创建临时文件夹的方法
2015/07/06 Python
python微信跳一跳系列之棋子定位像素遍历
2018/02/26 Python
基于Pandas读取csv文件Error的总结
2018/06/15 Python
python安装requests库的实例代码
2019/06/25 Python
python中的反斜杠问题深入讲解
2019/08/12 Python
python实现操作文件(文件夹)
2019/10/31 Python
利用python在excel中画图的实现方法
2020/03/17 Python
Windows 平台做 Python 开发的最佳组合(推荐)
2020/07/27 Python
通用的Django注册功能模块实现方法
2021/02/05 Python
HTML5 FormData 方法介绍以及实现文件上传示例
2017/09/12 HTML / CSS
大学军训自我鉴定
2013/12/15 职场文书
生产部经理岗位职责
2013/12/16 职场文书
超市活动计划书
2014/04/24 职场文书
创意婚礼策划方案
2014/05/18 职场文书
民事赔偿协议书
2014/11/02 职场文书
工伤私了协议书范本
2014/11/24 职场文书
小学语文的各类谚语(70首)
2019/08/15 职场文书
js实现模拟购物商城案例
2021/05/18 Javascript
Python Matplotlib绘制动画的代码详解
2022/05/30 Python
MySQL添加索引特点及优化问题
2022/07/23 MySQL
MySQL性能指标TPS+QPS+IOPS压测
2022/08/05 MySQL