为什么 python 不利用 __iadd__ 求和和链接运算符?

Why doesn't python take advantage of __iadd__ for sum and chained operators?

我刚刚进行了一个有趣的测试:

~$ python3 # I also conducted this on python 2.7.6, with the same result
Python 3.4.0 (default, Apr 11 2014, 13:05:11) 
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Foo(object):
...     def __add__(self, other):
...         global add_calls
...         add_calls += 1
...         return Foo()
...     def __iadd__(self, other):
...         return self
...
>>> add_calls = 0
>>> a = list(map(lambda x:Foo(), range(6)))
>>> a[0] + a[1] + a[2]
<__main__.Foo object at 0x7fb588e6c400>
>>> add_calls
2
>>> add_calls = 0
>>> sum(a, Foo())
<__main__.Foo object at 0x7fb588e6c4a8>
>>> add_calls
6

显然,__iadd__方法比__add__方法效率更高,不需要分配新的class。如果我添加的对象足够复杂,这将创建不必要的新对象,可能会在我的代码中造成巨大的瓶颈。

我希望在 a[0] + a[1] + a[2] 中,第一个操作会调用 __add__,第二个操作会在新创建的对象上调用 __iadd__

为什么 python 不对此进行优化?

__add__ 方法可以自由 return 不同类型的对象,而 __iadd__ 如果使用就地语义, return self.他们在这里不需要 return 相同类型的对象,所以 sum() 不应该依赖 __iadd__.

的特殊语义

您可以使用 functools.reduce() function 自行实现您想要的功能:

from functools import reduce

sum_with_inplace_semantics = reduce(Foo.__iadd__, a, Foo())

演示:

>>> from functools import reduce
>>> class Foo(object):
...     def __add__(self, other):
...         global add_calls
...         add_calls += 1
...         return Foo()
...     def __iadd__(self, other):
...         global iadd_calls
...         iadd_calls += 1
...         return self
... 
>>> a = [Foo() for _ in range(6)]
>>> result = Foo()
>>> add_calls = iadd_calls = 0
>>> reduce(Foo.__iadd__, a, result) is result
True
>>> add_calls, iadd_calls
(0, 6)

提供了一个很好的解决方法,但我觉得有必要总结一下散落在评论中的点点滴滴的答案:

sum 函数主要用于不可变类型。执行除第一个就地添加之外的所有添加将在具有 __iadd__ 方法的对象上创建性能改进,但在更典型的情况下检查 __iadd__ 方法会导致性能损失。 Special cases aren't special enough to break the rules.

我还声明 __add__ 应该只在 a + b + c 中调用一次,其中 a + b 创建一个临时变量,然后在返回它之前调用 tmp.__iadd__(c)。但是,这会违反最小意外原则。

既然你正在写你的 class,你知道它 __add__ 也可以 return 同一个对象,不是吗?

因此,您可以使用 + 运算符和内置 sum:

将柯里化优化代码转换为 运行
>>> class Foo(object):
...     def __add__(self, other):
...         global add_calls
...         add_calls += 1
...         return self

(请注意不要将您的代码传递给期望“+”成为新对象的第三方函数)