上山打老虎 发表于 2021-6-24 09:59:27

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]
查看完整版本: python 元组的实现和探析