为什么单值元组加列表比附加数字更高效?
Why is single-value tuple plus list is more efficient than appending a number?
假设我们需要将单个数字 1
附加到数组 a
.
在Python中我们有5个明显的方法:
a.append(1)
a += [1]
a += 1,
a.extend((1,))
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_LIST
(LOAD_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
假设我们需要将单个数字 1
附加到数组 a
.
在Python中我们有5个明显的方法:
a.append(1)
a += [1]
a += 1,
a.extend((1,))
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_LIST
(LOAD_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