Коли "i + = x" відрізняється від "i = i + x" в Python?


212

Мені сказали, що +=можуть мати різні наслідки, ніж стандартні позначення i = i +. Чи є випадок, в якому він i += 1би відрізнявся від i = i + 1?


7
+=діє як extend()у випадку зі списками.
Ashwini Chaudhary

12
@AshwiniChaudhary Це досить тонка відмінність, враховуючи i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]це True. Багато розробників можуть не помітити, що id(i)зміни для однієї операції, але не для іншої.
kojiro

1
@kojiro - Хоча це тонка відмінність, я думаю, що це важливо.
mgilson

@mgilson це важливо, і тому я відчув, що потрібно пояснення. :)
kojiro

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поведінку.


18
Ну, +=дзвонить, __iadd__ якщо він існує , і відновлюється до додавання та перезавантаження іншим чином. Ось чому i = 1; i += 1працює, хоча його немає int.__iadd__. Але крім цієї мінорної нитки, чудові пояснення.
abarnert

4
@abarnert - Я завжди вважав, що int.__iadd__щойно дзвонив __add__. Я радий, що сьогодні дізнався щось нове :).
mgilson

@abarnert - я вважаю , може бути , щоб бути повним , x + yвиклики , y.__radd__(x)якщо x.__add__не існує (або повернення NotImplementedі xі yмають різні типи)
mgilson

Якщо ви дійсно хочете бути комплементарним, вам слід зазначити, що біт "якщо він існує" проходить через звичні механізми getttr, за винятком деяких химерностей із класичними класами, а для типів, реалізованих у API C, він замість цього шукає або nb_inplace_addабо sq_inplace_concat, і ці функції API API мають суворіші вимоги, ніж методи Pynder, і ... Але я не думаю, що це стосується відповіді. Основна відмінність полягає в тому, що +=намагаються зробити додавання на місці, перш ніж повернутися до такої поведінки, як +я думаю, ви вже пояснили.
abarnert

Так, я гадаю, ти маєш рацію ... Хоча я міг би просто відмовитися від позиції, що API 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__метод і an __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?
mgilson

Ну, i.__iadd__не дзвонить __add__; це те, i += 1що дзвонить __add__.
abarnert

Помилка ... Так, це я мав на увазі. Цікаво. Я не усвідомлював, що це робиться автоматично.
mgilson

3
Перша спроба насправді i = i.__iadd__(1)- iadd може змінити об'єкт на місці, але не обов'язково, і тому очікується повернення результату в будь-якому випадку.
lvc

Зауважте, що це означає, що 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.