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正则用法的简单实例
Jun 21 Python
浅析Python3爬虫登录模拟
Feb 07 Python
基于Python log 的正确打开方式
Apr 28 Python
python leetcode 字符串相乘实例详解
Sep 03 Python
Python 微信之获取好友昵称并制作wordcloud的实例
Feb 21 Python
Python 根据日志级别打印不同颜色的日志的方法示例
Aug 08 Python
Python基于pip实现离线打包过程详解
May 15 Python
python利用递归方法实现求集合的幂集
Sep 07 Python
Python类型转换的魔术方法详解
Dec 23 Python
python调用百度AI接口实现人流量统计
Feb 03 Python
python wsgiref源码解析
Feb 06 Python
Django实现WebSocket在线聊天室功能(channels库)
Sep 25 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
深入理解require与require_once与include以及include_once的区别
2013/06/05 PHP
[原创]php求圆周率的简单实现方法
2016/05/30 PHP
php实现文件与16进制相互转换的方法示例
2017/02/16 PHP
Thinkphp5框架中引入Markdown编辑器操作示例
2020/06/03 PHP
JQUERY CHECKBOX全选,取消全选,反选方法三
2008/08/30 Javascript
写出更好的JavaScript之undefined篇(上)
2009/11/22 Javascript
jquery实现的Accordion折叠面板效果代码
2015/09/02 Javascript
jQuery 3.0 的变化及使用方法
2016/02/01 Javascript
vue之数据交互实例代码
2017/06/20 Javascript
canvas基础绘制-绚丽倒计时的实例
2017/09/17 Javascript
谈谈JS中的!!
2017/12/07 Javascript
JavaScript获取移动设备型号的实现代码(JS获取手机型号和系统)
2018/03/10 Javascript
Vue.js 实现微信公众号菜单编辑器功能(一)
2018/05/08 Javascript
Vue2.0 实现移动端图片上传功能
2018/05/30 Javascript
vue使用rem实现 移动端屏幕适配
2018/09/26 Javascript
封装一下vue中的axios示例代码详解
2020/02/16 Javascript
[01:28:24]NAVI vs VG Supermajor 败者组 BO3 第三场 6.5
2018/06/06 DOTA
利用Python的Django框架中的ORM建立查询API
2015/04/20 Python
Python字符串拼接、截取及替换方法总结分析
2016/04/13 Python
Python tkinter事件高级用法实例
2018/01/31 Python
python修改list中所有元素类型的三种方法
2018/04/09 Python
tensorflow 1.0用CNN进行图像分类
2018/04/15 Python
Python如何使用PIL Image制作GIF图片
2020/05/16 Python
天猫精选:上天猫,就够了
2016/09/21 全球购物
伦敦高达60%折扣的钻石珠宝商:Purely Diamonds
2018/06/24 全球购物
教育学专业毕业生的自我评价
2013/11/21 职场文书
岗位廉洁从政承诺书
2014/03/27 职场文书
竞选班干部演讲稿600字
2014/08/20 职场文书
人身损害赔偿协议书范本
2014/09/27 职场文书
青岛导游词
2015/02/12 职场文书
采购内勤岗位职责
2015/04/13 职场文书
小学教学工作总结2015
2015/05/13 职场文书
交通事故调解协议书
2015/05/20 职场文书
离婚律师函范本
2015/05/27 职场文书
Spring Boot mybatis-config 和 log4j 输出sql 日志的方式
2021/07/26 Java/Android
修改Nginx配置返回指定content-type的方法
2022/09/23 Servers