Почему python не использует __iadd__ для операторов суммирования и цепочки?

Я только что провёл интересный тест:

~$ 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__ метод, не требующий выделения нового класса. Если бы мои объекты были достаточно сложными, это создало бы ненужные новые объекты, потенциально создавая огромные узкие места в моем коде.

Я ожидаю, что в a[0] + a[1] + a[2], первая операция будет вызывать __add__и вторая операция будет вызывать __iadd__ на вновь созданном объекте.

Почему Python не оптимизирует это?

3 ответа

Решение

__add__ метод может свободно возвращать другой тип объекта, в то время как __iadd__ должен, если используется семантика на месте, вернуть self, Они не обязаны возвращать объект такого же типа здесь, поэтому sum() не следует полагаться на особую семантику __iadd__,

Вы можете использовать functools.reduce() Функция для реализации желаемой функциональности самостоятельно:

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__ метод приведет к потере производительности в более типичном случае. Особые случаи не настолько особенные, чтобы нарушать правила.

Я также заявил, что __add__ вероятно, следует вызывать только один раз в a + b + c, где a + b создает временную переменную, а затем вызывает tmp.__iadd__(c) прежде чем вернуть его. Однако это нарушит принцип наименьшего удивления.

Так как вы пишете свой класс в любом случае, вы знаете, что это __add__ Вы можете вернуть тот же объект, не так ли?

И поэтому вы можете сделать свой оптимизированный код карри для запуска как с + оператор и встроенный sum:

>>> class Foo(object):
...     def __add__(self, other):
...         global add_calls
...         add_calls += 1
...         return self

(Остерегайтесь передачи вашего кода сторонним функциям, которые ожидают, что "+" будет новым объектом)

Другие вопросы по тегам