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

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


person MarJamRob    schedule 13.03.2013    source источник
comment
+= действует как extend() в случае списков.   -  person Ashwini Chaudhary    schedule 13.03.2013
comment
@AshwiniChaudhary Это довольно тонкое различие, учитывая, что i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6] это True. Многие разработчики могут не заметить, что id(i) меняется для одной операции, но не для другой.   -  person kojiro    schedule 13.03.2013
comment
@kojiro - Хотя это тонкое различие, я думаю, что оно важное.   -  person mgilson    schedule 13.03.2013
comment
@mgilson это важно, поэтому я почувствовал, что это требует объяснения. :)   -  person kojiro    schedule 13.03.2013
comment
Связанный вопрос о различиях между ними в Java: stackoverflow.com/a/7456548/245966   -  person jakub.g    schedule 19.03.2013
comment
возможный дубликат Что делает плюс равно (+=) в Python?   -  person Martijn Pieters    schedule 06.02.2014


Ответы (3)


Это полностью зависит от объекта 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.

person mgilson    schedule 13.03.2013
comment
Итак, += вызывает __iadd__, если он существует, и возвращается к добавлению и повторному связыванию в противном случае. Вот почему i = 1; i += 1 работает, хотя int.__iadd__ нет. Но кроме этой мелкой гниды, отличные объяснения. - person abarnert; 13.03.2013
comment
@abarnert - я всегда предполагал, что int.__iadd__ только что позвонил __add__. Я рад, что узнал что-то новое сегодня :). - person mgilson; 13.03.2013
comment
@abarnert - я полагаю, что, возможно, для complete x + y вызывает y.__radd__(x), если x.__add__ не существует (или возвращает NotImplemented, x и y разных типов) - person mgilson; 13.03.2013
comment
Если вы действительно хотите быть полным, вы должны упомянуть, что бит, если он существует, проходит через обычные механизмы getattr, за исключением некоторых причуд с классическими классами, и для типов, реализованных в C API, он вместо этого ищет либо nb_inplace_add, либо sq_inplace_concat, и к этим функциям C API предъявляются более строгие требования, чем к методам dunder Python, и… Но я не думаю, что это имеет отношение к ответу. Основное отличие состоит в том, что += пытается выполнить добавление на месте, прежде чем вернуться к действию, аналогичному +, что, я думаю, вы уже объяснили. - person abarnert; 13.03.2013
comment
Да, я полагаю, вы правы... Хотя я мог бы просто вернуться к позиции, что C API не является частью python. Это часть Cpython :-P - person mgilson; 13.03.2013
comment
Чтобы еще больше усложнить обсуждение комментариев: x + y пытается type(y).__radd__ сначала, если issubclass(type(y), type(x)). - person wim; 25.01.2017
comment
@wim -- Спасибо за этот комментарий, я добавил дополнительный абзац внизу... - person mgilson; 25.01.2017
comment
Что вы имеете в виду [] поверх i... [.]? - person Kindred; 22.11.2018
comment
Я имею в виду, что новому объекту будет присвоено имя i -- что бы там ни было ранее, оно будет на один шаг ближе к сборке мусора. - person mgilson; 29.11.2018
comment
@wim Это тоже кажется не совсем правильным, поскольку класс считается подклассом самого себя (я думаю, точно так же, как набор является подмножеством самого себя, а строка является подстрокой самого себя) и выполнение x + y с одинаковыми типами сначала пытается __add__. - person Kelly Bundy; 15.09.2020
comment
@HeapOverflow Странно, что класс считается подклассом самого себя, я не понимаю причин этого. Конечно, я должен квалифицироваться как строгий подкласс. Ваша точка зрения также указывает на ошибку документации в примечании здесь. - person wim; 15.09.2020
comment
Добавлен github.com/python/cpython/pull/22257 для ошибки документации. - person wim; 15.09.2020

Под прикрытием 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, к которому он уже был привязан, поэтому обычно вы можете игнорировать эту часть.)

person abarnert    schedule 13.03.2013
comment
действительно ли __iadd__ вызывает __add__ в случае AttributeError? - person mgilson; 13.03.2013
comment
Ну, i.__iadd__ не звонит __add__; это i += 1 звонит __add__. - person abarnert; 13.03.2013
comment
эээ... Да, это то, что я имел в виду. Интересно. Я не знал, что это делается автоматически. - person mgilson; 13.03.2013
comment
Первая попытка на самом деле i = i.__iadd__(1) - iadd может изменить объект на месте, но не обязана, поэтому ожидается, что результат будет возвращен в любом случае. - person lvc; 13.03.2013
comment
Обратите внимание, что это означает, что operator.iadd вызывает __add__ для AttributeError, но не может повторно связать результат... поэтому i=1; operator.iadd(i, 1) возвращает 2, а i оставляет равным 1. Что немного сбивает с толку. - person abarnert; 13.03.2013
comment
@lvc: Вы правы, но это еще больше сбивает с толку. Вы можете использовать даже return что-то отличное от self. Позвольте мне посмотреть, как прояснить это в ответе. - person abarnert; 13.03.2013
comment
@JBernardo: Да, += всегда перепривязывает переменную, а operator.iadd — нет. Вот почему в документах прямо говорится, что a = iadd(a, b) эквивалентно a += b, а не iadd(a, b) эквивалентно a += b. - person abarnert; 13.03.2013

Вот пример, который напрямую сравнивает 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]
person Deqing    schedule 23.08.2014