Коли "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це на самому справі зміни 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поведінку.


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.