【问题标题】:How is Python's List Implemented?Python 的 List 是如何实现的?
【发布时间】:2011-04-24 11:20:35
【问题描述】:

是链表还是数组?我四处寻找,只发现人们在猜测。我的 C 知识还不够好,无法查看源代码。

【问题讨论】:

  • According to docs ,Python 列表不是链表。它们是可变大小的数组。它们也是可变的。我不确定它是否真的实现了逻辑和真正的容量(这将使它成为一个完整的 dynamic array 。所以你可以说它是一个独特的数据结构。(虽然我真的相信它是一个动态数组)

标签: python arrays list linked-list python-internals


【解决方案1】:

这取决于实现,但 IIRC:

  • CPython 使用指针数组
  • Jython 使用 ArrayList
  • IronPython 显然也使用数组。您可以浏览source code 了解详情。

因此它们都有 O(1) 随机访问。

【讨论】:

  • 依赖于实现的python解释器将列表实现为链表将是python语言的有效实现吗?换句话说:不能保证 O(1) 随机访问列表?难道不依赖实现细节就无法编写高效的代码吗?
  • @sepp 我相信 Python 中的列表只是有序集合;所述实施的实施和/或性能要求没有明确说明
  • @sppe2k:由于 Python 并没有真正的标准或正式规范(尽管有一些文档说“......保证......”),你不能100% 确定,如“这是由某张纸保证的”。但是由于O(1) 用于列表索引是一个非常普遍且有效的假设,因此没有实现敢于破坏它。
  • @Paul 它没有说明列表的底层实现应该如何完成。
  • 只是没有指定事物的大 O 运行时间。语言语法规范不一定与实现细节的含义相同,只是经常发生这种情况。
【解决方案2】:

在 CPython 中,列表是指针数组。 Python 的其他实现可能会选择以不同的方式存储它们。

【讨论】:

    【解决方案3】:

    这是一个dynamic array。实际证明:无论索引如何,索引都需要(当然差异非常小(0.0013 µsecs!))相同的时间:

    ...>python -m timeit --setup="x = [None]*1000" "x[500]"
    10000000 loops, best of 3: 0.0579 usec per loop
    
    ...>python -m timeit --setup="x = [None]*1000" "x[0]"
    10000000 loops, best of 3: 0.0566 usec per loop
    

    如果 IronPython 或 Jython 使用链表,我会感到震惊 - 它们会破坏许多基于列表是动态数组的假设而广泛使用的库的性能。

    【讨论】:

    • @Ralf:我知道我的 CPU(大多数其他硬件也是如此)很旧而且速度很慢 - 从好的方面来说,我可以假设对我来说运行速度足够快的代码也足够快对于所有用户:D
    • @delnan:-1 您的“实际证明”是一派胡言,6 个赞成票也是如此。大约 98% 的时间都花在了 x=[None]*1000 上,因此对任何可能的列表访问差异的测量都相当不精确。您需要分离出初始化:-s "x=[None]*100" "x[0]"
    • 表明它不是一个简单的链表实现。没有明确表明它是一个数组。
    • 结构远不止链表和数组,时间对于在它们之间做出决定没有实际用处。
    【解决方案4】:

    实际上,C 代码非常简单。扩展一个宏,剪掉一些不相关的cmets,基本结构在listobject.h,定义了一个列表为:

    typedef struct {
        PyObject_HEAD
        Py_ssize_t ob_size;
    
        /* Vector of pointers to list elements.  list[0] is ob_item[0], 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
         */
        Py_ssize_t allocated;
    } PyListObject;
    

    PyObject_HEAD 包含一个引用计数和一个类型标识符。所以,它是一个过度分配的向量/数组。当数组已满时调整数组大小的代码在listobject.c 中。它实际上并没有使数组加倍,而是通过分配来增长

    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
    new_allocated += newsize;
    

    每次到容量,其中newsize 是请求的大小(不一定是allocated + 1,因为您可以extend 任意数量的元素,而不是append 一个接一个)。

    另请参阅Python FAQ

    【讨论】:

    • 所以,当迭代 python 列表时,它和链表一样慢,因为每个条目都只是一个指针,所以每个元素很可能会导致缓存未命中。
    • @Kr0e:如果后续元素实际上是同一个对象,则不是 :) 但如果您需要更小/缓存友好的数据结构,则首选 array 模块或 NumPy。
    • @Kr0e 我不会说迭代列表和链表一样慢,但是迭代链表的 比链表慢,以及 Fred 提到的警告。例如,遍历一个列表以将其复制到另一个列表应该比链表更快。
    【解决方案5】:

    根据documentation

    Python 的列表实际上是可变长度数组,而不是 Lisp 样式的链表。

    【讨论】:

      【解决方案6】:

      正如其他人在上面所说,列表(当相当大时)是通过分配固定数量的空间来实现的,如果该空间应该填满,则分配更多的空间并复制元素。

      要理解为什么该方法是 O(1) 摊销,但不失一般性,假设我们已插入 a = 2^n 个元素,现在我们必须将表加倍到 2^(n+1) 大小。这意味着我们目前正在进行 2^(n+1) 次操作。最后一个副本,我们做了 2^n 次操作。在此之前,我们做了 2^(n-1)... 一直到 8,4,2,1。现在,如果我们把这些加起来,我们得到 1 + 2 + 4 + 8 + ... + 2^(n+1) = 2^(n+2) - 1

      【讨论】:

      • 据我了解,没有复制旧元素。分配了更多空间,但新空间与已使用的空间不连续,只有要插入的较新元素被复制到新空间。如果我错了,请纠正我。
      【解决方案7】:

      我建议Laurent Luce's article "Python list implementation"。对我来说真的很有用,因为作者解释了列表是如何在 CPython 中实现的,并为此使用了出色的图表。

      列表对象C结构

      CPython 中的列表对象由以下 C 结构表示。 ob_item 是指向列表元素的指针列表。分配的是内存中分配的槽数。

      typedef struct {
          PyObject_VAR_HEAD
          PyObject **ob_item;
          Py_ssize_t allocated;
      } PyListObject;
      

      注意分配的插槽和列表大小之间的差异很重要。列表的大小与len(l) 相同。分配的插槽数是在内存中分配的。通常,您会看到已分配可能大于大小。这是为了避免每次将新元素附加到列表时都需要调用realloc

      ...

      追加

      我们将一个整数附加到列表中:l.append(1)。会发生什么?

      我们继续添加一个元素:l.append(2)list_resize 以 n+1 = 2 调用,但由于分配的大小为 4,因此无需分配更多内存。当我们再添加 2 个整数时也会发生同样的情况:l.append(3)l.append(4)。下图显示了我们目前所拥有的。

      ...

      插入

      让我们在位置 1 插入一个新整数 (5):l.insert(1,5),然后看看内部发生了什么。

      ...

      流行音乐

      当你弹出最后一个元素时:l.pop()listpop() 被调用。 list_resizelistpop() 内部调用,如果新大小小于分配大小的一半,则列表将缩小。

      您可以观察到插槽 4 仍然指向整数,但重要的是列表的大小现在是 4。 让我们再弹出一个元素。在list_resize() 中,size – 1 = 4 – 1 = 3 小于分配槽的一半,因此列表缩减为 6 个槽,现在列表的新大小为 3。

      您可以观察到插槽 3 和 4 仍然指向一些整数,但重要的是列表的大小现在是 3.

      ...

      删除 Python 列表对象有一个删除特定元素的方法:l.remove(5).

      【讨论】:

      • 谢谢,我现在更了解列表的链接部分了。 Python 列表是 aggregation,而不是 composition。我希望也有一份作曲清单。
      【解决方案8】:

      Python 中的列表类似于数组,您可以在其中存储多个值。 List 是可变的,这意味着您可以更改它。您应该知道的更重要的事情是,当我们创建一个列表时,Python 会自动为该列表变量创建一个 reference_id。如果您通过分配其他变量来更改它,则主列表将被更改。让我们尝试一个例子:

      list_one = [1,2,3,4]
      
      my_list = list_one
      
      #my_list: [1,2,3,4]
      
      my_list.append("new")
      
      #my_list: [1,2,3,4,'new']
      #list_one: [1,2,3,4,'new']
      

      我们附加了my_list,但我们的主列表已更改。那意味着的列表没有分配为副本列表分配为其参考。

      【讨论】:

        【解决方案9】:

        在 CPython 中列表是作为动态数组实现的,因此当我们追加时,不仅添加了一个宏,而且分配了更多空间,因此每次都不应添加新空间。

        【讨论】:

          猜你喜欢
          • 2017-06-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多