投射到 Python 中的列表时的内存分配

Memory allocation when casting to a list in Python

我知道当你在 python 中追加到一个列表时,为列表分配的内存量会慢慢增加。当您达到预分配部分的限制时,将分配新内存并复制现有项目。

将迭代器转换为列表时会发生什么?它执行相同的过程还是更智能?

你将实现 __len__ 方法的东西投射到列表中,它是否分配了 obj.__len__() 内存槽以减少投射过程中的复制量?

综上所述,就列表 list_of_interest 的内存分配方式而言,以下三个片段之间是否存在差异?

标准追加

list_of_interest = []
for i in range(1000):
    list_of_interest.append(i)

转换迭代器

list_of_interest = list(range(1000))

__len__

铸造对象
b = tuple(range(1000))
list_of_interest = list(b)

感谢您的帮助!

当定义 __len__ 时,list(至少,在 CPython 中;其他实现可能不同)将使用 iterable 报告的大小来预分配一个足够大的数组来容纳所有 iterable 的元素.

您的第二个和第三个示例实际上是相同的,因为 range 提供 __len__(因为计算一个范围内的整数个数很简单具有定义的终点)。

不能定义__len__的可迭代对象的一个​​例子是filter。即使 filter 的实例知道 its 可迭代参数有多大,它也不知道在不实际迭代它的情况下会产生多少参数。所以像 list(filter(odd, range(1000))) 这样的东西必须以一个空列表开始并作为 filter returns 奇数附加到它。

有一个 __length_hint__ 容器可以定义的方法来至少提供关于它们包含多少项目的估计。不过,我在 listobject.c 中没有看到任何证据表明 list 将使用它来预分配其数组。

通过使用 dis 模块反汇编每个代码示例,我们可以更深入地了解正在发生的事情。

标准追加:

dis.dis("""
list_of_interest = []
for i in range(1000):
    list_of_interest.append(i)
""")

给我们

  2           0 BUILD_LIST               0
              2 STORE_NAME               0 (list_of_interest)

  3           4 SETUP_LOOP              26 (to 32)
              6 LOAD_NAME                1 (range)
              8 LOAD_CONST               0 (1000)
             10 CALL_FUNCTION            1
             12 GET_ITER
        >>   14 FOR_ITER                14 (to 30)
             16 STORE_NAME               2 (i)

  4          18 LOAD_NAME                0 (list_of_interest)
             20 LOAD_METHOD              3 (append)
             22 LOAD_NAME                2 (i)
             24 CALL_METHOD              1
             26 POP_TOP
             28 JUMP_ABSOLUTE           14
        >>   30 POP_BLOCK
        >>   32 LOAD_CONST               1 (None)
             34 RETURN_VALUE

仔细观察我们发现我们正在循环调用 append 方法(就像我们写的一样)。深入研究实现 (here),我们看到这最终调用 app1,后者调用 list_resize,后者过度分配以尝试减少重新分配。

其他两个例子:

如其他答案所述 range 实际上确实实现了 __len__ making

list_of_interest = list(range(1000))

b = tuple(range(1000))
list_of_interest = list(b)

工作方式非常相似。

第一个反汇编给出:

  1           0 LOAD_NAME                0 (list)
              2 LOAD_NAME                1 (range)
              4 LOAD_CONST               0 (1000)
              6 CALL_FUNCTION            1
              8 CALL_FUNCTION            1
             10 STORE_NAME               2 (list_of_interest)
             12 LOAD_CONST               1 (None)
             14 RETURN_VALUE

第二个:

  2           0 LOAD_NAME                0 (tuple)
              2 LOAD_NAME                1 (range)
              4 LOAD_CONST               0 (1000)
              6 CALL_FUNCTION            1
              8 CALL_FUNCTION            1
             10 STORE_NAME               2 (b)

  3          12 LOAD_NAME                3 (list)
             14 LOAD_NAME                2 (b)
             16 CALL_FUNCTION            1
             18 STORE_NAME               4 (list_of_interest)
             20 LOAD_CONST               1 (None)
             22 RETURN_VALUE

在这两种情况下,我们调用 list 定义的构造函数 here。我们看到,如果提供给 list 的可迭代对象有一个长度,那么我们调用 list_preallocate_exact,它会按照您的预期运行。否则我们在空列表上调用 list_extend。如果我们的迭代器不是列表或元组(在这种情况下我们知道它不是),我们将调用 PyObject_LengthHint 来猜测迭代器的长度并以这种方式预分配 space .由于长度提示可能太大或太小,我们可能需要在以标准方式循环遍历迭代器时分配更多内存,并且我们可能会在完成循环后释放 space太大了。

其他情况:

我们可以尝试其他示例,但我们看到我们将使用上面的 list 构造函数,它有几个特殊情况。通常我们接下来会转到 list_extend,它也有几个特殊情况。在我们的迭代器没有实现长度或长度提示的情况下,we default to a length hint of 8。为什么是8?你得问维护者那个。

结论:

如果Python从一开始就知道结果列表的所需长度,您将只有一个分配。如果它必须猜测,您可能有多个或一个重新分配。如果在循环追加或提供没有长度或长度提示的迭代器的情况下无法猜测,您将有多个分配。