为什么单值元组加列表比附加数字更高效?

Why is single-value tuple plus list is more efficient than appending a number?

假设我们需要将单个数字 1 附加到数组 a.

在Python中我们有5个明显的方法:

  1. a.append(1)
  2. a += [1]
  3. a += 1,
  4. a.extend((1,))
  5. a.extend([1])

让我们用timeit来测量它:

from timeit import timeit

print(timeit("a.append(1)", "a = []", number=10_000_000))
print(timeit("a += [1]", "a = []", number=10_000_000))
print(timeit("a += 1,", "a = []", number=10_000_000))
print(timeit("a.extend((1,))", "a = []", number=10_000_000))
print(timeit("a.extend([1])", "a = []", number=10_000_000))

这是控制台的输出:

5.05412472199896
5.869792026000141
3.1280645619990537
4.988895307998973
8.05588494499898

为什么第三个比其他的更有效率?

元组和列表的区别完全在于创建它们所花费的时间不同。这很容易通过自己计时 list/tuple 创建,以及独立于正在添加的对象的创建计时 += 操作来验证:

>>> timeit("b = [1]", number=10_000_000)
0.447727799997665
>>> timeit("b = (1,)", number=10_000_000)
0.17419059993699193
>>> timeit("a += b", "a, b = [], [1]", number=10_000_000)
0.5244328000117093
>>> timeit("a += b", "a, b = [], (1,)", number=10_000_000)
0.5320590999908745

编译器优化了元组 (1,) 的创建。另一方面,总是创建列表。看看dis.dis

>>> import dis
>>> dis.dis('a.extend((1,))')
  1           0 LOAD_NAME                0 (a)
              2 LOAD_METHOD              1 (extend)
              4 LOAD_CONST               0 ((1,))
              6 CALL_METHOD              1
              8 RETURN_VALUE
>>> dis.dis('a.extend([1])')
  1           0 LOAD_NAME                0 (a)
              2 LOAD_METHOD              1 (extend)
              4 LOAD_CONST               0 (1)
              6 BUILD_LIST               1
              8 CALL_METHOD              1
             10 RETURN_VALUE

注意,它需要更少的 byte-code 指令,并且仅在 (1,) 上执行 LOAD_CONST。另一方面,对于列表,调用 BUILD_LISTLOAD_CONST 表示 1)。

注意,您可以在代码对象上访问这些常量:

>>> code = compile('a.extend((1,))', '', 'eval')
>>> code
<code object <module> at 0x10e91e0e0, file "", line 1>
>>> code.co_consts
((1,),)

最后,关于为什么 +=.extend 快,好吧,再看一下字节码:

>>> dis.dis('a += b')
  1           0 LOAD_NAME                0 (a)
              2 LOAD_NAME                1 (b)
              4 INPLACE_ADD
              6 STORE_NAME               0 (a)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
>>> dis.dis('a.extend(b)')
  1           0 LOAD_NAME                0 (a)
              2 LOAD_METHOD              1 (extend)
              4 LOAD_NAME                2 (b)
              6 CALL_METHOD              1
              8 RETURN_VALUE

您会注意到 .extend,它需要首先 解析该方法(这需要额外的时间)。另一方面,使用运算符有它自己的字节码:INPLACE_ADD 所以所有东西都被推到 C 层(另外,魔术方法跳过实例名称空间和一堆喧嚣,直接在 class).

好的,总结一下,@juanpa.arrivillaga、@Samwise 和@Barmar 提到的内容:

a += (1, ) 等同于 a.__iadd__((1, )) 但没有加载方法。如果我们看 dis:

>>> dis.dis("a.__iadd__((1,))")
  1           0 LOAD_NAME                0 (a)
              2 LOAD_METHOD              1 (__iadd__)
              4 LOAD_CONST               0 ((1,))
              6 CALL_METHOD              1
              8 RETURN_VALUE
>>> dis.dis("a += (1, )")
  1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 ((1,))
              4 INPLACE_ADD
              6 STORE_NAME               0 (a)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
>>> dis.dis("a.append(1)")
  1           0 LOAD_NAME                0 (a)
              2 LOAD_METHOD              1 (append)
              4 LOAD_CONST               0 (1)
              6 CALL_METHOD              1
              8 RETURN_VALUE

你可以看到,在第一种和第三种情况下,我们需要在调用前使用 LOAD_METHOD,这是大部分 resourse-mean 部分,而 += 有直接反汇编程序

顺便说一句,第一个案例比前五个案例更糟糕,并且准时 8.292503296999712