Python中的元组容器序列(tuple)与列表容器序列(List)具有极大的相似之处,因此也常被称为不可变的列表。
但是两者之间也有很多的差距,元组侧重于数据的展示,而列表侧重于数据的存储与 *** 作。
它们非常相似,虽然都可以存储任意类型的数据,但是一个元组定义好之后就不能够再进行修改。
元组特性元组的特点:
元组属于容器序列元组属于不可变类型元组底层由顺序存储组成,而顺序存储是线性结构的一种@H_404_20@基本声明以下是使用类实例化的形式进行对象声明:
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, 5print("值:%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, ("三", "四"))print("值:%r,类型:%r" % (tup, type(tup)))# 值:(1, 2, ('三', '四')),类型:<class 'tuple'>
续行 *** 作在Python中,元组中的数据项如果过多,可能会导致整个元组太长,太长的元组是不符合PEP8规范的。
每行最大的字符数不可超过79,文档字符或者注释每行不可超过72Python虽然提供了续行符\,但是在元组中可以忽略续行符,如下所示:
tup = ( 1, 2, 3, 4, 5)print("值:%r,类型:%r" % (tup, type(tup)))# 值:(1, 2, 3, 4, 5),类型:<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种情况中的哪一种?
t 变成 (1, 2, [30, 40, 50, 60])。因为 tuple 不支持对它的数据项赋值,所以会抛出 TypeError 异常。以上两个都不是。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() | ... | 详情参见函数章节 |
使用len()方法来获取元组的长度。
返回int类型的值。
tup = ("A", "B", "C", "D", "E", "F", "G")print(len(tup))# 7
Python在对内置的数据类型使用len()方法时,实际上是会直接的从PyVarObject结构体中获取ob_size属性,这是一种非常高效的策略。
PyVarObject是表示内存中长度可变的内置对象的C语言结构体。
直接读取这个值比调用一个方法要快很多。
统计次数使用count()方法统计数据项在该元组中出现的次数。
返回int:
tup = ("A", "B", "C", "D", "E", "F", "G", "A")aInTupCount = tup.count("A")print(aInTupCount)# 2
查找位置使用index()方法找到数据项在当前元组中首次出现的位置索引值,如数据项不存在则抛出异常。
返回int。
tup = ("A", "B", "C", "D", "E", "F", "G", "A")aInTupIndex = tup.index("A")print(aInTupIndex)# 0
底层探究@H_502_280@内存开辟Python内部实现中,列表和元组还是有一定的差别的。
元组在创建对象申请内存的时候,内存空间大小便进行了固定,后续不可更改(如果是传入了一个可迭代对象,例如tupe(range(100)),这种情况会进行扩容与缩容,下面的章节将进行探讨研究)。
而列表在创建对象申请内存的时候,内存空间大小不是固定的,如果后续对其新增或删除数据项,列表会进行扩容或者缩容机制。
元组创建空元组若创建一个空元组,会直接进行创建,然后将这个空元组丢到缓存free_List中。
元组的free_List最多能缓存 20 * 2000 个元组,这个在下面会进行讲解。
如图所示:
元组转元组这样的代码会进行元组转元组:
tup = tuple((1, 2, 3))
首先内部本身就是一个元组(1, 2, 3),所以会直接将内部的这个元组拿出来并返回引用,并不会再次创建。
代码验证:
>>> oldTup = (1, 2, 3)>>> ID(oldTup)4384908128>>> newTup = tuple(oldTup)>>> ID(newTup)4384908128>>>
列表转元组列表转元组会将列表中的每一个数据项都拿出来,然后放入至元组中:
tup = tuple([1, 2, 3])
所以你会发现,列表和元组中的数据项引用都是相同的:
>>> li1 = ["A", "B", "C"]>>> tup = tuple(li1)>>> print(ID(li1[0]))4383760656>>> print(ID(tup[0]))4383760656>>>
可迭代对象转元组可迭代对象是没有长度这一概念的,如果是可迭代对象转换为元组,会先对可迭代对象的长度做一个猜想。
并且根据这个猜想,为元组开辟一片内存空间,用于存放可迭代对象的数据项。
然后内部会获取可迭代对象的迭代器,对其进行遍历 *** 作,拿出数据项后放至元组中。
如果猜想的长度太小,会导致元组内部的内存不够存放下所有的迭代器数据项,此时该元组会进行内部的扩容机制,直至可迭代对象中的数据项全部被添加至元组中。
rangeObject = range(1, 101)tup = tuple(rangeObject)// 假如猜想的是9// 第一步:+ 10 // 第二步:+ (原长度+10) * 0.25// 其实,就是增加【原长度*0.25 + 2.5】
如果猜想的长度太大,而实际上迭代器中的数据量偏少,则需要对该元组进行缩容。
切片取值对元组进行切片取值的时候,会开辟一个新元组用于存放切片后得到的数据项。
tup = (1, 2, 3)newSliceTup = tup[0:2]
当然,如果是[:]的 *** 作,则参照绝对引用,直接返回被切片的元组引用。
代码验证:
>>> ID(tup)4384908416>>> newSliceTup = tup[0:2]>>> ID(newSliceTup)4384904392
缓存机制free_List缓存元组的缓存机制和列表的缓存机制不同。
元组的free_List会缓存0 - 19长度的共20种元组,其中每一种长度的元组通过单向链表横向扩展缓存至2000个,如下图所示:
当每一次的del *** 作有数据项的元组时,都会将该元组数据项清空并挂载至free_List单向链表的头部的位置。
del 元组1del 元组2del 元组3
如下图所示:
当要创建一个元组时,会通过创建元组的长度,从free_List单向链表的头部取出一个元组,然后将数据项存放进去。
前提是free_List单向链表中缓存的有该长度的元组。
tup = (1, 2, 3)
@H_200_404@空元组与非空元组的缓存空元组的缓存是一经创建就缓存到free_List单向链表中。
而非空元组的缓存必须是del *** 作后才缓存到free_List单向链表中。
空元组的创建第一次创建空元组后,空元组会缓存至free_List单向链表中。
以后的每一次空元组创建,返回的其实都是同一个引用,也就是说空元组在free_List单向链表中即使被引用了也不会被销毁。
>>> t1 = ()>>> ID(t1)4511088712>>> t2 = ()>>> ID(t2)4511088712
非空元组的创建当free_List单向链表中有相同长度的元组时,会进行引用并删除。
这个在上图中已经示例过了,就是这个:
代码示例:
$ python3Python 3.6.8 (v3.6.8:3c6b436a57, Dec 24 2018, 02:04:31)[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwinType "help", "copyright", "credits" or "license" for more information.>>> v1 = (None, None, None)>>> ID(v1)4384907696>>> v2 = (None, None, None)>>> ID(v2)4384908056>>> del v1>>> del v2 # ①>>> v3 = (None, None, None)>>> ID(v3) # ②4384908056>>> v4 = (None, None, None)>>> ID(v4) # ③4384907696>>>
①:free_List num_free=3 单向链表结构:v2 —> v1
②:创建了v3,拿出v2的空元组,填入v3数据项,故v2和v3的ID值相等,证明引用同一个元组,此时free_List num_free=3 单向链表结构为:—> v1
③:创建了v4,拿出v1的空元组,填入v4数据项,故v1和v4的ID值相等,证明引用同一个元组
tupleobject.c源码官网参考:点我跳转
源码一览:点我跳转
以下是截取了一些关键性源代码,并且做上了中文注释,方便查阅。
每一个元组都有几个关键性的属性:
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 voIDtupledealloc(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所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)