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


212

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


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его, он действительно изменяется b(и aвидит это изменение тоже - в конце концов, он ссылается на тот же список). Во втором случае, однако, когда я это делаю 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.


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


на __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]
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.