Зачем возвращать что-либо, кроме `self` из` __iadd__`?

Python документация о методах, связанных с операторами на месте, например += и *= (или, как она их называет, расширенные арифметические задания) имеет следующее:

Эти методы должны пытаться выполнить операцию на месте (модифицировать self) и вернуть результат (который может быть, но не должен быть, self). Если определенный метод не определен, расширенное присваивание возвращается к нормальным методам.

У меня есть два тесно связанных вопроса:

  • Почему необходимо возвращать что-либо из этих методов, если в документации указано, что, если они будут реализованы, они все равно должны делать что-то на месте? Почему операторы расширенного присваивания просто не выполняют избыточное присваивание в случае, когда реализована __iadd__?
  • При каких обстоятельствах имеет смысл возвращать что-то отличное от self из расширенного метода присваивания?

Небольшое экспериментирование показывает, что неизменяемые типы Python не реализуют __iadd__ (что согласуется с цитируемой документацией):

>>> x = 5
>>> x.__iadd__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__iadd__'

и методы __iadd__ его изменяемых типов, конечно, работают на месте и возвращают self:

>>> list1 = []
>>> list2 = list1
>>> list1 += [1,2,3]
>>> list1 is list2
True

Как таковой, я не могу понять, для чего нужна способность возвращать вещи, отличные от self от __iadd__. Похоже, было бы неправильно делать абсолютно все обстоятельства.

Ответы

Ответ 1

Почему необходимо возвращать что-либо из этих методов, если в документации указано, что, если они будут реализованы, они все равно должны делать что-то на месте? Почему операторы расширенного присваивания просто не выполняют избыточное присваивание в случае реализации __iadd__?

Одна из причин заключается в том, чтобы заставить их быть операторами вместо выражений.


Большей причиной является то, что задание не всегда является излишним. В случае, когда левая часть - это просто переменная, обязательно, после мутации объекта, повторное связывание этого объекта с именем, к которому оно уже привязано, обычно не требуется.

Но как насчет случая, когда левая сторона является более сложной целью назначения? Помните, что вы можете назначать и дополнять-присваивать подпискам, срезам и атрибутам, например a[1] += 2 или a.b -= 2. В этом случае вы на самом деле вызываете __setitem__ или __setattr__ для объекта, а не просто привязываете переменную.


Кроме того, стоит отметить, что "избыточное присвоение" - не совсем дорогая операция. Это не С++, где любое присваивание может вызвать вызов пользовательского оператора присваивания для значения. (Это может привести к вызову пользовательского оператора сеттера на объекте, что значение является элементом, подслоем или атрибутом, и это может быть дорогостоящим... но в этом случае оно не является избыточным, как объяснялось выше.)


И последняя причина напрямую связана с вашим вторым вопросом: вы почти всегда хотите вернуть self из __ispam__, но почти всегда это не всегда. И если __iadd__ никогда не возвращался self, назначение было бы явно необходимым.


При каких обстоятельствах имеет смысл возвращать что-то другое, кроме себя, из расширенного метода присваивания?

Вы просмотрели важный связанный бит здесь:

Эти методы должны пытаться выполнять операцию на месте (модифицировать self)

В любом случае, когда они не могут выполнять операцию на месте, но могут делать что-то еще, вероятно, будет разумно вернуть что-то другое, кроме self.

Представьте себе объект, который использовал реализацию копирования на запись, мутируя на месте, если он был единственной копией, но в противном случае создавал новую копию. Вы не можете этого сделать, не выполнив __iadd__ и не позволяя += вернуться к __add__; вы можете сделать это, выполнив __iadd__, который может создавать и возвращать копию вместо мутирования и возврата self. (Вы можете сделать это по соображениям производительности, но также возможно, что у вас будет объект с двумя разными интерфейсами, "высокоуровневый" интерфейс выглядит неизменным и копирует на запись, в то время как "низкоуровневый" интерфейс раскрывает фактическое разделение.)

Итак, первая причина, по которой это необходимо, - это обработать случай, не относящийся к делу.


Но есть ли другие причины? Конечно.

Одна из причин заключается только в том, чтобы обернуть другие языки или библиотеки, где это важная функция.

Например, в Objective C многие методы возвращают self, который обычно, но не всегда тот же объект, который получил вызов метода. Это не всегда означает, что ObjC обрабатывает такие вещи, как кластеры классов. В Python есть лучшие способы сделать одно и то же (даже изменение класса во время выполнения обычно лучше), но в ObjC это совершенно нормально и идиоматично. (Он используется только для методов init в Apple current Framework, но это соглашение их стандартной библиотеки, что методы mutator, добавленные NSMutableFoo, всегда возвращают void, как и соглашение, что методы мутатора, такие как list.sort, всегда возвращают None в Python, а не в части языка.) Итак, если вы хотите завершить работу среды выполнения ObjC в Python, как бы вы справились с этим?

Вы можете добавить дополнительный прокси-сервер перед всем, поэтому ваш объект-обертка может изменить объект ObjC, который он обертывает. Но это означает много сложного кода делегирования (особенно если вы хотите, чтобы работа ObjC отразилась с помощью оболочки в Python) и код управления памятью, а также производительность.

Вместо этого вы можете просто иметь общую тонкую обертку. Если вы вернете другой объект ObjC, чем вы начали, вы вернете оболочку вокруг этой вещи вместо обертки вокруг той, с которой вы начали. Тривиальный код, управление памятью автоматически, без затрат на производительность. Пока пользователи вашей обертки всегда делают a += b вместо a.__iadd__(b), они не будут видеть разницы.

Я понимаю, что "писать оболочку стиля PyObjC вокруг другой библиотеки фреймворков ObjC, чем Apple Foundation" - это не совсем повседневный случай использования... но вы уже знали, что это функция, которую вы не используете каждый день, так что еще вы ожидаете?

Простой ленивый прокси-сервер сети может сделать что-то похожее - начните с крошечного объекта moniker, замените его на полный прокси-объект при первом попытке что-то сделать с ним. Вероятно, вы можете думать о других подобных примерах. Вы, вероятно, никогда не напишете ни одного из них... но если вам нужно, вы могли бы.

Ответ 2

Чтобы ответить на второй вопрос, вы возвращаете что-то другое, кроме себя, когда имеете дело с неизменяемыми объектами.

Возьмем этот пример:

>>> foo = tuple([1, 2, 3])
>>> foo
(1, 2, 3)

Теперь я пытаюсь изменить объект на месте:

>>> foo += (4,)
>>> foo
(1, 2, 3, 4)

Не могу поверить, что сработало!... теперь мы пытаемся вызвать его метод __iadd__:

>>> foo.__iadd__((5,))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute '__iadd__'

У него его нет! Странно... теперь вы должны удивляться самому себе, что случилось, когда мы сделали foo += (4,)?

Хорошо, что произошло, так это то, что при попытке такой модификации места на неизменяемых объектах python использует объект __add__ для выполнения операции, т.е. создается новый объект. Так что на самом деле произошло то, что когда мы это сделали:

>>> foo += (4,)

Это произошло:

>>> foo = foo + (4,)

Смотрите это

Поэтому, когда вы пытаетесь использовать += или любую другую модификацию места на неизменяемом объекте, python будет использовать не мутирующий эквивалент этого оператора, и вы вернете новый объект. Другой пример:

>>> foo += (5,) + (6,)
>>> foo
(1, 2, 3, 4, 5, 6)

Вышеприведенное означает:

foo = foo + (5,) + (6,)

Надеюсь, что ответит на ваш вопрос.