python 元组的实现和探析
其实就表面感官来说,元组和列表的样子大同小异,面试中经常会遇到的, tuple和 list 有什么区别?这种问题几乎都问烂了,大部分人可以回答的七七八八了,什么 tuple不能变, list可以进行增删改; tuple创建是通过 (), list是通过[],短短两句话道尽其功能与生成,然而道不尽其本质与性能,其丰富的内涵还需要细细展开与推演。源码解析对比
首先来分析 list 列表,它的具体结构如下所示:
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements.list is ob_item, etc. */
PyObject **ob_item;
/* ob_item contains space for 'allocated' elements.The number
* currently in use is ob_size.
* Invariants:
* 0 <= ob_size <= allocated
* len(list) == ob_size
* ob_item == NULL implies ob_size == allocated == 0
* list.sort() temporarily sets allocated to -1 to detect mutations.
*
* Items must normally not be NULL, except during construction when
* the list is not yet visible outside the function that builds it.
*/
Py_ssize_t allocated;
} PyListObject; 有兴趣的读者,可直接阅读 list 列表实现的源码文件listobject.h 和 listobject.c。
在最近的一篇文章中我们分析到 list 本质上是一个长度可变的连续数组,其中 ob_item是一个指针列表,里边的每一个指针都指向列表中的元素,而 allocated则用于存储该列表目前已被分配的空间大小。需要注意的是 allocated 和列表的实际空间大小不同,列表实际空间大小,指的是 len(list)返回的结果,也就是上边代码中注释中的ob_size,表示该列表总共存储了多少个元素,而在实际情况中,为了优化存储结构,避免每次增加元素都要重新分配内存,列表预分配的空间 allocated 往往会大于 ob_size。
因此 allocated 和 ob_size 的关系是: allocated >= len(list) = ob_size >= 0。
如果当前列表分配的空间已满(即 allocated == len(list)),则会向系统请求更大的内存空间,并把原来的元素全部拷贝过去。
接下来再分析元组,如下所示为 Python 3.7 tuple 元组的具体结构:
typedef struct {
PyObject_VAR_HEAD
PyObject *ob_item;
/* ob_item contains space for 'ob_size' elements.
* Items must normally not be NULL, except during construction when
* the tuple is not yet visible outside the function that builds it.
*/
} PyTupleObject; 有兴趣的读者,可阅读 tuple元组实现的源码文件 tupleobject.h 和 tupleobject.c。
他的 memory layout 如下所示
[*] free_list 用于存储长度为 0 的 tuple 对象,整个解释器的生命周期里面只有一个长度为 0 的 tuple 对象实例
[*] free_list 用于存储长度为 1 的 tuple 对象,可以通过 tuple 对象的 ob_item 指针遍历到下一个长度为 1 的 tuple 对象
[*] free_list 用于存储长度为 2 的 tuple 对象,上图画的 free_list 中只有一个对象
[*] free_list 每一格最多能存储 PyTuple_MAXFREELIST个 tuple 链表长度,这里定义的是 2000
我们来看下 PyTuple_New 函数
/* Objects/tupleobject.c 79 - 136 行 */
PyObject *
PyTuple_New(Py_ssize_t size)
{
PyTupleObject *op;
Py_ssize_t i;
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
/* 如果启动了 free_list 存储 */
#if PyTuple_MAXSAVESIZE > 0
if (size == 0 && free_list) {
/* 如果 size 为 0, 则返回 free_list 的第一个元素 */
op = free_list;
Py_INCREF(op);
#ifdef COUNT_ALLOCS
_Py_tuple_zero_allocs++;
#endif
return (PyObject *) op;
}
if (size < PyTuple_MAXSAVESIZE && (op = free_list) != NULL) {
/* 如果 size 在 free_list 范围内,并且 free_list 存有之前回收过的对应size的tuple 对象
把 free_list 指向 op 的下一个链表地址
*/
free_list = (PyTupleObject *) op->ob_item;
numfree--;
#ifdef COUNT_ALLOCS
_Py_fast_tuple_allocs++;
#endif
/* Inline PyObject_InitVar */
#ifdef Py_TRACE_REFS
Py_SIZE(op) = size;
Py_TYPE(op) = &PyTuple_Type;
#endif
_Py_NewReference((PyObject *)op);
}
else
#endif
{
/* free_list 未找到对应大小的 tuple 并且 size 不为 0,向操作系统分配 */
/* Check for overflow */
if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - sizeof(PyTupleObject) -
sizeof(PyObject *)) / sizeof(PyObject *)) {
return PyErr_NoMemory();
}
op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
if (op == NULL)
return NULL;
}
/* 把 tuple 里面的元素设置为空指针 */
for (i=0; i < size; i++)
op->ob_item = NULL;
#if PyTuple_MAXSAVESIZE > 0
if (size == 0) {
free_list = op;
++numfree;
Py_INCREF(op); /* extra INCREF so that this is never freed */
}
#endif
#ifdef SHOW_TRACK_COUNT
count_tracked++;
#endif
_PyObject_GC_TRACK(op);
return (PyObject *) op;
} 从函数名也大概可以猜出功能了
元组真的不能修改吗?
嘿嘿,这倒不一定,比如说进行以下操作
>>> t = (,)
>>> t
(, )
>>> t.append("hello yerik")
>>> t
(, )
>>> t.pop()
'hello yerik'
>>> t.pop(2)
5
>>> t
(, ) 这个tuple定义的时候有2个元素,都是两个list。不是说tuple一旦定义后就不可变了吗?怎么后来又变了?
别急别急,案例中的t包含两个元素,都是list
页:
[1]