Почему 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
(Остерегайтесь передачи вашего кода сторонним функциям, которые ожидают, что "+" будет новым объектом)