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中运行并行任务技巧
Feb 26 Python
解析Python中的二进制位运算符
May 13 Python
Python算法应用实战之队列详解
Feb 04 Python
Pycharm之快速定位到某行快捷键的方法
Jan 20 Python
使用python读取.text文件特定行的数据方法
Jan 28 Python
python可视化篇之流式数据监控的实现
Aug 07 Python
python通过SSH登陆linux并操作的实现
Oct 10 Python
双向RNN:bidirectional_dynamic_rnn()函数的使用详解
Jan 20 Python
Django模板之基本的 for 循环 和 List内容的显示方式
Mar 31 Python
Python 字典一个键对应多个值的方法
Sep 29 Python
解决pycharm不能自动保存在远程linux中的问题
Feb 06 Python
python 制作本地应用搜索工具
Feb 27 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
新52大事件
2020/03/03 欧美动漫
如何限制访问者的ip(PHPBB的代码)
2006/10/09 PHP
PHP 第二节 数据类型之数值型
2012/04/28 PHP
ThinkPHP跳转页success及error模板实例教程
2014/07/17 PHP
PHP结合jQuery插件ajaxFileUpload实现异步上传文件实例
2020/08/17 PHP
PHP+Ajax 检测网络是否正常实例详解
2016/12/16 PHP
用Laravel轻松处理千万级数据的方法实现
2020/12/25 PHP
juery框架写的弹窗效果适合新手
2013/11/27 Javascript
JavaScript人脸识别技术及脸部识别JavaScript类库Tracking.js
2015/09/14 Javascript
jQuery实现的调整表格行tr上下顺序
2016/01/10 Javascript
jquery对dom节点的操作【推荐】
2016/04/15 Javascript
jQuery Easyui 验证两次密码输入是否相等
2016/05/13 Javascript
最常见的左侧分类菜单栏jQuery实现代码
2016/11/28 Javascript
详解Jquery的事件操作和文档操作
2016/12/19 Javascript
js实现带三角符的手风琴效果
2017/03/01 Javascript
webpack 2.x配置reactjs基本开发环境详解
2017/08/08 Javascript
Vue自定义指令实现checkbox全选功能的方法
2018/02/28 Javascript
vue2单元测试环境搭建
2018/05/24 Javascript
微信小程序chooseImage的用法(从本地相册选择图片或使用相机拍照)
2018/08/22 Javascript
微信小程序自定义导航教程(兼容各种手机)
2018/12/12 Javascript
详解Vue iview IE浏览器不兼容报错(Iview Bable polyfill)
2019/01/07 Javascript
基于JS实现视频上传显示进度条
2020/05/12 Javascript
[51:22]Fnatic vs IG 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
[05:31]干嘛呢兄弟!DOTA2 TI9语音轮盘部分出处
2019/05/14 DOTA
python数据处理 根据颜色对图片进行分类的方法
2018/12/08 Python
python实现AES和RSA加解密的方法
2019/03/28 Python
Python动态语言与鸭子类型详解
2019/07/01 Python
Python散点图与折线图绘制过程解析
2019/11/30 Python
基于pandas中expand的作用详解
2019/12/17 Python
Django 实现将图片转为Base64,然后使用json传输
2020/03/27 Python
Corelle官方网站:购买康宁餐具
2016/11/02 全球购物
三年级数学教学反思
2014/01/31 职场文书
物业管理委托协议(2篇)
2014/09/23 职场文书
车间质检员岗位职责
2015/04/08 职场文书
学雷锋活动简报
2015/07/20 职场文书
2016大学生暑期三下乡心得体会
2016/01/23 职场文书