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语言实现获取主机名根据端口杀死进程
Mar 31 Python
浅析Python中的赋值和深浅拷贝
Aug 15 Python
Python序列化基础知识(json/pickle)
Oct 19 Python
python中闭包Closure函数作为返回值的方法示例
Dec 17 Python
TensorFlow入门使用 tf.train.Saver()保存模型
Apr 24 Python
在python 中实现运行多条shell命令
Jan 07 Python
Python使用Beautiful Soup爬取豆瓣音乐排行榜过程解析
Aug 15 Python
使用python+whoosh实现全文检索
Dec 09 Python
在Python中利用pickle保存变量的实例
Dec 30 Python
tensorflow 获取checkpoint中的变量列表实例
Feb 11 Python
Python3.7将普通图片(png)转换为SVG图片格式(网站logo图标)动起来
Apr 21 Python
Python初识逻辑与if语句及用法大全
Aug 07 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 preg_match_all结合str_replace替换内容中所有img
2008/10/11 PHP
php中把美国时间转为北京时间的自定义函数分享
2014/07/28 PHP
php从memcache读取数据再批量写入mysql的方法
2014/12/29 PHP
php+ajax实现无刷新的新闻留言系统
2020/12/21 PHP
Laravel中使用Queue的最基本操作教程
2017/12/27 PHP
php学习笔记之mb_strstr的基本使用
2018/02/03 PHP
php+ajax实现无刷新文件上传功能(ajaxuploadfile)
2018/02/11 PHP
让firefox支持IE的一些方法的javascript扩展函数代码
2010/01/02 Javascript
json-lib出现There is a cycle in the hierarchy解决办法
2010/02/24 Javascript
javascript完美拖拽的实现方法
2013/09/29 Javascript
jQuery动画特效实例教程
2014/08/29 Javascript
JS来动态的修改url实现对url的增删查改
2014/09/05 Javascript
基于js里调用函数时,函数名带括号和不带括号的区别
2016/07/28 Javascript
javascript实现搜索筛选功能实例代码
2020/11/12 Javascript
python开发环境PyScripter中文乱码问题解决方案
2016/09/11 Python
python 数据清洗之数据合并、转换、过滤、排序
2017/02/12 Python
Python读取MRI并显示为灰度图像实例代码
2018/01/03 Python
pandas系列之DataFrame 行列数据筛选实例
2018/04/12 Python
python实现逆序输出一个数字的示例讲解
2018/06/25 Python
python+influxdb+shell编写区域网络状况表
2018/07/27 Python
对python中字典keys,values,items的使用详解
2019/02/03 Python
Python爬虫实现爬取百度百科词条功能实例
2019/04/05 Python
python项目对接钉钉SDK的实现
2019/07/15 Python
wxPython实现整点报时
2019/11/18 Python
常用的10个Python实用小技巧
2020/08/10 Python
html2canvas截图空白问题的解决
2020/03/24 HTML / CSS
工作说明书范文
2014/05/07 职场文书
安全生产演讲稿
2014/05/09 职场文书
电子商务专业应届生求职信
2014/05/28 职场文书
工程负责人任命书
2014/06/06 职场文书
医院标语大全
2014/06/23 职场文书
干部个人对照检查材料
2014/08/25 职场文书
出国签证在职证明
2014/09/20 职场文书
逃课检讨书范文
2015/05/06 职场文书
【js设计模式】SOLID五大设计原则
2022/03/24 Javascript
纯CSS打字动画的实现示例
2022/08/05 HTML / CSS