是什么导致[* a]总体化?


136

显然list(a)不是总归,[x for x in a]在某些时候[*a]总归,始终都是总归吗?

大小上限为n = 100

这是从0到12的大小n,以及三种方法的结果大小(以字节为单位):

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

这样计算,可以使用Python 3 在repl.it中重现。8

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

因此:这是如何工作的?如何整体化[*a]?实际上,它使用什么机制从给定的输入创建结果列表?它是否使用了迭代器a并使用了类似的东西list.append?源代码在哪里?

产生图像的数据和代码协作。)

放大到较小的n:

大小上限为n = 40

缩小到较大的n:

大小最大为n = 1000


1
接下来,扩展您的测试用例,似乎列表理解的行为就像编写一个循环并将每个项目附加到列表中,而[*a]似乎表现为extend在空列表中使用。
jdehesa

4
查看为每个生成的字节码可能会有所帮助。list(a)完全在C中运行;它可以在迭代时逐个节点分配内部缓冲区a[x for x in a]只是使用LIST_APPEND了很多东西,所以它遵循普通列表的正常“过度分配一点,必要时重新分配”的模式。[*a]使用BUILD_LIST_UNPACK,这...我不知道该怎么做,除了显然一直都在分配:)
chepner

2
此外,在Python 3.7中,看起来list(a)[*a]完全相同,并且与相比,二者总体上相同[x for x in a],因此... sys.getsizeof可能不是在此处使用的正确工具。
chepner

7
@chepner我认为这sys.getsizeof是正确的工具,它只是显示了list(a)用于整体化的工具。实际上,Python 3.8的新增功能提到了这一点:“列表构造函数不会整体分配[...]”
Stefan Pochmann

5
@chepner:那是3.8中修复的错误;构造函数不应该整体化。
ShadowRanger

Answers:


81

[*a] 在内部执行C等效于

  1. 新建一个空的 list
  2. 呼叫 newlist.extend(a)
  3. 返回list

因此,如果您将测试扩展到:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

在线尝试!

你会看到的结果getsizeof([*a])l = []; l.extend(a); getsizeof(l)是相同的。

通常这是正确的做法;在extending时,您通常希望以后再添加,对于一般的解压缩,类似的情况是,假设要在一个接一个的基础上添加多个内容。[*a]这不是正常情况;Python假定list[*a, b, c, *d])中添加了多个项目或可迭代项,因此在常见情况下,过度分配可以节省工作。

相比之下,list由单个预先确定的可迭代大小(带有list())构成的结构在使用过程中可能不会增长或收缩,并且积算过早,除非另外证明。Python最近修复了一个错误,该错误使构造函数甚至可以对已知大小的输入进行整体分配

至于list理解,它们实际上等效于重复的appends,因此当您一次添加一个元素时,您会看到正常的过度分配增长模式的最终结果。

需要明确的是,这都不是语言保证。这就是CPython实现它的方式。Python语言规范通常与特定的增长模式无关list(除了保证从末期开始摊销O(1) appends和pops)。如评论中所述,具体实现在3.9中再次更改;尽管这不会影响[*a],但可能会影响其他情况,这些情况曾经是“构建tuple单个项目的临时内容,然后extend使用tuple”,现在变成的多个应用程序LIST_APPEND,当发生超额分配以及要计算的数字是多少时,该应用程序可能会发生变化。


4
@StefanPochmann:我之前读过代码(这就是为什么我已经知道这一点的原因)。这是的字节码处理程序BUILD_LIST_UNPACK,它_PyList_Extend用作C的等效调用extend(只是直接使用,而不是通过方法查找)。他们将其与构建tuple带有拆包的路径相结合;tuples不能很好地用于零散构建,因此它们总是解压缩到a list(以受益于过度分配),并tuple在需要时最终转换为。
ShadowRanger

4
请注意,这显然改变了3.9,在建设有独立的字节码进行(BUILD_LISTLIST_EXTEND对于每一件事情来解压,LIST_APPEND在堆栈上构建整个前单品),而不是装载一切list与一个字节码指令(它允许编译器执行优化的所有功能于一身的指令并没有让,像实施[*a, b, *c]LIST_EXTENDLIST_APPENDLIST_EXTENDW / O需要包裹b在一个单tuple,以满足要求BUILD_LIST_UNPACK)。
ShadowRanger

18

的全貌是什么情况,建立在其他的答案和评论(尤其是ShadowRanger的答案,这也解释了为什么它这样做)。

拆卸显示已BUILD_LIST_UNPACK被使用:

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

这是在中ceval.c处理,它会构建一个空列表并将其扩展(带有a):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend 用途 list_extend

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

其中调用list_resize的总和为

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

overallocates如下:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

让我们检查一下。用上面的公式计算预期的点数,并通过将预期的字节数乘以8(如我在此处使用64位Python)并添加一个空列表的字节数(即列表对象的固定开销)来计算预期的字节数:

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

输出:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

匹配,除了n = 0list_extend实际上是快捷方式,因此实际上也匹配:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {

8

这些将是CPython解释器的实现细节,因此在其他解释器之间可能不一致。

也就是说,您可以list(a)在这里看到理解和行为的位置:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

专为理解:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

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

在这些行的下面,有list_preallocate_exact调用时使用的行list(a)


1
[*a]不会一次附加单个元素。它有自己的专用字节码,可通过进行批量插入extend
ShadowRanger

Gotcha-我想我对此还没有深入研究。删除了[*a]
Randy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.