Когда «i + = x» отличается от «i = i + x» в Python?

212

Мне сказали, что +=могут иметь разные эффекты, чем стандартные обозначения i = i +. Есть ли случай, в котором i += 1будет отличаться от i = i + 1?

MarJamRob
источник
7
+=действует как extend()в случае списков.
Ашвини Чаудхари
12
@AshwiniChaudhary Это довольно тонкое различие, учитывая, что i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]это так True. Многие разработчики могут не заметить, что id(i)изменения для одной операции, но не для другой.
Кодзиро
1
@kojiro - Хотя это тонкое различие, я думаю, что оно важное.
Мгилсон
@mgilson это важно, и поэтому я чувствовал, что нужно объяснение. :)
Кодзиро
1
Связанный вопрос, касающийся различий между ними в Java: stackoverflow.com/a/7456548/245966
jakub.g

Ответы:

317

Это полностью зависит от объекта i.

+=вызывает __iadd__метод (если он существует - возвращается к нему, __add__если он не существует), тогда как в некоторых случаях+ вызывает __add__метод 1 или __radd__метод 2 .

С точки зрения API, __iadd__предполагается использовать для изменения изменяемых объектов на месте (возвращая объект, который был мутирован), тогда как __add__должен возвращать новый экземпляр чего-либо. Для неизменяемых объектов оба метода возвращают новый экземпляр, но __iadd__помещают новый экземпляр в текущее пространство имен с тем же именем, что и у старого экземпляра. Вот почему

i = 1
i += 1

похоже на прирост i. В действительности вы получаете новое целое число и назначаете его «поверх», iтеряя одну ссылку на старое целое число. В этом случае i += 1точно так же, как i = i + 1. Но с большинством изменяемых объектов это другая история:

В качестве конкретного примера:

a = [1, 2, 3]
b = a
b += [1, 2, 3]
print a  #[1, 2, 3, 1, 2, 3]
print b  #[1, 2, 3, 1, 2, 3]

по сравнению с:

a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print a #[1, 2, 3]
print b #[1, 2, 3, 1, 2, 3]

обратите внимание на то, как в первом примере, bи aкогда я ссылаюсь +=на один и тот же объект, когда я использую bего, он действительно изменяется baвидит это изменение тоже - в конце концов, он ссылается на тот же список). Во втором случае, однако, когда я это делаю b = b + [1, 2, 3], он берет список, который bссылается, и объединяет его с новым списком [1, 2, 3]. Затем он сохраняет объединенный список в текущем пространстве имен как b- безотносительно к тому, какая bстрока была раньше.


1 В выражении x + y, если x.__add__не выполняются или если x.__add__(y)возврат NotImplemented и xи yимеют разные типы , то x + yпытается вызов y.__radd__(x). Итак, в случае, если у вас есть

foo_instance += bar_instance

если Fooне реализует __add__или __iadd__то результат здесь такой же, как

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2 В выражении foo_instance + bar_instance, bar_instance.__radd__будет судим , foo_instance.__add__ если тип bar_instanceявляется подклассом типа foo_instance(например issubclass(Bar, Foo)). Рационально это потому , что Barв каком - то смысле «более высокого уровня» объекта , чем Fooтак Barдолжны получить возможность переопределения Fooповедения «s.

mgilson
источник
18
Ну, +=вызывает, __iadd__ если он существует , и в противном случае возвращается к добавлению и повторному связыванию. Вот почему i = 1; i += 1работает, хотя нет int.__iadd__. Но кроме этой незначительной гниды, отличные объяснения.
abarnert
4
@abarnert - я всегда предполагал, что int.__iadd__только что звонил __add__. Я рад, что узнал что-то новое сегодня :).
Мгилсон
@abarnert - я полагаю , может быть , чтобы быть полным , x + yвызовы , y.__radd__(x)если x.__add__не существует (или возврат NotImplementedи xи yимеют различные типы)
mgilson
Если вы действительно хотите быть законченным, вам следует упомянуть, что бит «если он существует» проходит через обычные механизмы getattr, за исключением некоторых причуд с классическими классами, а для типов, реализованных в C API, вместо этого он ищет либо nb_inplace_addили sq_inplace_concat, и те функции C API имеют более строгие требования, чем методы Python, и ... Но я не думаю, что это имеет отношение к ответу. Основное различие заключается в том, что он +=пытается сделать добавление на месте, прежде чем вернуться к действию подобного +, что, я думаю, вы уже объяснили.
abarnert
Да, я полагаю, вы правы ... Хотя я мог бы просто отступить от позиции, что C API не является частью Python . Это часть Cpython :-P
mgilson
67

Под прикрытием i += 1делает что-то вроде этого:

try:
    i = i.__iadd__(1)
except AttributeError:
    i = i.__add__(1)

Пока i = i + 1делает что-то вроде этого:

i = i.__add__(1)

Это небольшое упрощение, но вы понимаете: Python предоставляет типам способ обработки +=, создавая как __iadd__метод, так и метод __add__.

Предполагается, что изменяемые типы, например list, будут мутировать сами __iadd__(и затем возвращаться self, если вы не делаете что-то очень сложное), в то время как неизменяемые типы, например int, просто не будут реализовывать это.

Например:

>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

Потому что l2это тот же объект l1, что и вы мутировали l1, вы также мутировали l2.

Но:

>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

Здесь вы не мутировали l1; вместо этого вы создали новый список l1 + [3]и отскочили от имени, l1чтобы указать на него, оставив l2указатель на исходный список.

+=версии вы также связывали l1, просто в этом случае вы связывали его с тем же, с которым listон уже был связан, так что вы обычно можете игнорировать эту часть.)

abarnert
источник
на __iadd__самом деле звонить __add__в случае AttributeError?
Мгилсон
Ну, i.__iadd__не звонит __add__; это i += 1те звонки __add__.
abarnert
ээээ ... да, это то, что я имел в виду Интересный. Я не осознавал, что это было сделано автоматически.
Мгилсон
3
Первая попытка на самом деле i = i.__iadd__(1)- iadd может изменить объект на месте, но не обязан, и поэтому ожидается, что в любом случае вернет результат.
13:30
Обратите внимание , что это означает , что operator.iaddвызовы __add__на AttributeError, но он не может Пересвяжите результат ... так i=1; operator.iadd(i, 1)возвращается 2 и листьев iустановлен 1. Что немного сбивает с толку.
abarnert
6

Вот пример, который напрямую сравнивается i += xс i = i + x:

def foo(x):
  x = x + [42]

def bar(x):
  x += [42]

c = [27]
foo(c); # c is not changed
bar(c); # c is changed to [27, 42]
Дэцин
источник