我正在阅读《Python学习手册第5版》,我需要对这段话进行更多的解释:
例如,字符串的__add__方法实际上执行连接操作;尽管Python在内部将以下第一个映射到第二个,但通常不应该使用第二种形式(它不太直观,而且可能运行得更慢):
>>> S+'NI!'
'spamNI!'
>>> S.__add__('NI!')
'spamNI!'
所以我的问题是,为什么它会运行得更慢?
我正在阅读《Python学习手册第5版》,我需要对这段话进行更多的解释:
例如,字符串的__add__方法实际上执行连接操作;尽管Python在内部将以下第一个映射到第二个,但通常不应该使用第二种形式(它不太直观,而且可能运行得更慢):
>>> S+'NI!'
'spamNI!'
>>> S.__add__('NI!')
'spamNI!'
>>> def test(a, b):
... return a + b
...
>>> def test2(a, b):
... return a.__add__(b)
...
>>> import dis
>>> dis.dis(test)
2 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_ADD
7 RETURN_VALUE
>>> dis.dis(test2)
2 0 LOAD_FAST 0 (a)
3 LOAD_ATTR 0 (__add__)
6 LOAD_FAST 1 (b)
9 CALL_FUNCTION 1
12 RETURN_VALUE
BINARY_ADD
指令代替 2 条指令:LOAD_ATTR
和 CALL_FUNCTION
。由于 BINARY_ADD
做的事情(几乎)相同(但是用 C 实现),所以我们可以期望它会稍微快一点。不过这个差别几乎不会被注意到。LEA
指令可以用其他一系列指令来替代。但是它们的表现不如LEA
好。+
理论上应该比__add__
更快,但是一定要进行测量。+
和 __add__
的微基准测试结果来选择。很可能存在许多“更糟糕”的性能问题,这两种加法方式之间的差异只是其中之一。 - poke+
运算符实际上会在幕后调用 __add__
。因此,当你执行 S + 'NI!'
时,幕后实际上是调用了 __add__
(如果 S
存在的话)。从语义上讲,两个版本都完全相同。dis
模块查看字节码。>>> import dis
>>> dis.dis("S+'NI!'")
1 0 LOAD_NAME 0 (S)
2 LOAD_CONST 0 ('NI!')
4 BINARY_ADD
6 RETURN_VALUE
>>> dis.dis("S.__add__('NI!')")
1 0 LOAD_NAME 0 (S)
2 LOAD_METHOD 1 (__add__)
4 LOAD_CONST 0 ('NI!')
6 CALL_METHOD 1
如您所见,这里的区别基本上是 +
运算符仅执行 BINARY_ADD
,而 __add__
调用则加载实际方法并执行它。
当解释器看到 BINARY_ADD
时,会自动查找 __add__
的实现并调用它,但是它可以比在 Python 字节码中查找方法更高效地完成此操作。
因此,通过显式调用 __add__
,您阻止了解释器使用更快的路线到达实现。
话虽如此,差异微不足道。如果您计时两个调用之间的差异,您可以看到差异,但它确实不是很大(这是 1000 万次调用):
>>> timeit("S+'NI!'", setup='S = "spam"', number=10**7)
0.45791053899995404
>>> timeit("S.__add__('NI!')", setup='S = "spam"', number=10**7)
1.0082074819999889
__add__
实现),对__add__
的调用可能会更快:>>> timeit("S+'NI!'", setup='from __main__ import SType;S = SType()', number=10**7)
0.7971681049998551
>>> timeit("S.__add__('NI!')", setup='from __main__ import SType;S = SType()', number=10**7)
0.6606798959999196
+
确实较慢。+
。如果您需要关注性能,则需要分析整个应用程序,并且不要信任这种微基准测试。当查看您的应用程序时,它们并没有帮助,而在99.99%的情况下,这两种方法之间的差异不会有影响。更有可能的是,您的应用程序中存在其他瓶颈,它们将使其变慢。+
进行了极致优化。 - deceze__add__
)让我非常好奇,所以我开始深入研究它。理论上来说,自定义与否不应该有影响,对吧?我做了很多测试,有时候+
更快,有时候__add__
更快。似乎__add__
的实现并不重要。但是当它是自定义的时候,它会严重占据执行时间。而且由于差异非常小(在我的机器上进行1000万轮后加减100毫秒左右),因此我认为这是由于外部(例如非Python)因素造成的,可以安全地假设如此。 - freakish+
实际上不仅仅调用 __add__
方法;当我们说两者是等效的时,这只是一种简化(因为 +
也会寻找其他方法来调用)。另一个原因可能是从本地执行 BINARY_ADD
切换回执行 Python 代码会对性能产生一些小影响。 - poke