问题:是什么导致[* a]总体化?
显然list(a)
不是总归,[x for x in a]
在某些时候[*a]
总归,始终都是总归吗?
这是从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:
回答 0
[*a]
在内部执行C等效于:
- 新建一个空的
list
- 呼叫
newlist.extend(a)
- 返回
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)
是相同的。
通常这是正确的做法;在extend
ing时,您通常希望以后再添加,对于一般的解压缩,类似的情况是,假设要在一个接一个的基础上添加多个内容。[*a]
这不是正常情况;Python假定list
([*a, b, c, *d]
)中添加了多个项目或可迭代项,因此在常见情况下,过度分配可以节省工作。
相比之下,list
由单个预先确定的可迭代大小(带有list()
)构成的结构在使用过程中可能不会增长或收缩,并且积算过早,除非另外证明。Python最近修复了一个错误,该错误使构造函数甚至可以对已知大小的输入进行整体分配。
至于list
理解,它们实际上等效于重复的append
s,因此当您一次添加一个元素时,您会看到正常的过度分配增长模式的最终结果。
需要明确的是,这都不是语言保证。这就是CPython实现它的方式。Python语言规范通常与特定的增长模式无关list
(除了保证从末期开始摊销O(1)
append
s和pop
s)。如评论中所述,具体实现在3.9中再次更改;尽管这不会影响[*a]
,但可能会影响其他情况,这些情况曾经是“构建tuple
单个项目的临时内容,然后extend
使用tuple
”,现在变成的多个应用程序LIST_APPEND
,当发生超额分配以及要计算的数字是多少时,该应用程序可能会发生变化。
回答 1
的全貌是什么情况,建立在其他的答案和评论(尤其是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_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 = 0
,list_extend
实际上是快捷方式,因此实际上也匹配:
if (n == 0) {
...
Py_RETURN_NONE;
}
...
if (list_resize(self, m + n) < 0) {
回答 2
这些将是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)
。