Kiedy „i + = x” różni się od „i = i + x” w Pythonie?

212

Powiedziano mi, że +=może mieć inne efekty niż standardowa notacja i = i +. Czy istnieje przypadek, w którym i += 1byłby inny i = i + 1?

MarJamRob
źródło
7
+=zachowuje się jak extend()w przypadku list.
Ashwini Chaudhary
12
@AshwiniChaudhary To całkiem subtelne rozróżnienie, biorąc pod uwagę, że tak i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]jest True. Wielu programistów może nie zauważyć id(i)zmian dla jednej operacji, ale nie dla drugiej.
kojiro
1
@kojiro - Chociaż jest to subtelne rozróżnienie, myślę, że jest ważne.
mgilson
@mgilson to ważne, więc czułem, że potrzebuje wyjaśnienia. :)
kojiro
1
Podobne pytanie dotyczące różnic między nimi w Javie: stackoverflow.com/a/7456548/245966
jakub.g

Odpowiedzi:

317

Zależy to całkowicie od obiektu i.

+=wywołuje __iadd__metodę (jeśli istnieje - wycofuje się, __add__jeśli nie istnieje), podczas gdy +wywołuje __add__metodę 1 lub __radd__metodę w kilku przypadkach 2 .

Z perspektywy API, __iadd__ma być używany do modyfikowania obiektów podlegających mutacji w miejscu (zwracanie zmutowanego obiektu), podczas gdy __add__powinien zwracać nową instancję czegoś. W przypadku obiektów niezmiennych obie metody zwracają nową instancję, ale __iadd__umieszczą nową instancję w bieżącej przestrzeni nazw o tej samej nazwie, co stara instancja. Dlatego

i = 1
i += 1

wydaje się zwiększać i. W rzeczywistości dostajesz nową liczbę całkowitą i przypisujesz ją „na wierzchu” i- tracąc jedno odniesienie do starej liczby całkowitej. W tym przypadku i += 1jest dokładnie taki sam jak i = i + 1. Ale w przypadku większości zmiennych obiektów jest to inna historia:

Jako konkretny przykład:

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]

w porównaniu do:

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

Zauważ, jak w pierwszym przykładzie, ponieważ bi aodwoływać się do tego samego obiektu, kiedy używam +=na b, to faktycznie zmienia b(i awidzi, że zmiany też - Po tym wszystkim, to odwołuje się do tej samej listy). Jednak w drugim przypadku, gdy to zrobię b = b + [1, 2, 3], pobiera listę, która bsię do niej odwołuje, i łączy ją z nową listą [1, 2, 3]. Następnie przechowuje skonkatenowaną listę w bieżącej przestrzeni nazw jako b- Bez względu na bpoprzednią linię.


1 W wyrażeniu x + y, jeśli x.__add__nie jest realizowany lub jeśli x.__add__(y)powróci NotImplemented i xi ymają różne rodzaje , a następnie x + ystara się rozmowy y.__radd__(x). Tak więc w przypadku, gdy masz

foo_instance += bar_instance

jeśli Foosię nie implementuje __add__lub __iadd__wynik tutaj jest taki sam jak

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2 W wyrażeniu foo_instance + bar_instance, bar_instance.__radd__będzie próbował wcześniej foo_instance.__add__ czy typ bar_instancejest podklasą typu foo_instance(na przykład issubclass(Bar, Foo)). Racjonalne jest to, bo Barjest w pewnym sensie „wyższy poziom” obiekt niż Footak Barpowinny uzyskać możliwość nadrzędnymi Foo„s zachowanie.

mgilson
źródło
18
Cóż, +=nazywa __iadd__ jeśli istnieje , i wraca do dodawania i ponownego wiązania inaczej. Dlatego i = 1; i += 1działa, chociaż nie ma int.__iadd__. Ale poza tym drobną nitką, świetne wyjaśnienia.
abarnert
4
@abarnert - zawsze zakładałem, że int.__iadd__właśnie zadzwoniłem __add__. Cieszę się, że nauczyłem się dziś czegoś nowego :).
mgilson
@abarnert - Przypuszczam, że może być pełna , x + ypołączeń y.__radd__(x)jeśli x.__add__nie istnieje (lub zwrotów NotImplementedi xi ysą różnych typów)
mgilson
Jeśli naprawdę chcesz być kompletnym, musisz wspomnieć, że bit „jeśli istnieje” przechodzi przez zwykłe mechanizmy getattr, z wyjątkiem niektórych dziwactw z klasycznymi klasami, a dla typów zaimplementowanych w C API szuka zamiast tego nb_inplace_addlub sq_inplace_concatte funkcje C API mają bardziej rygorystyczne wymagania niż metody dundera Pythona i… Ale nie sądzę, żeby to miało znaczenie w odpowiedzi. Główne rozróżnienie polega na tym +=, że próbuje zrobić coś w miejscu, zanim wrócisz do działania w podobny sposób +, co myślę, że już wyjaśniłeś.
abarnert
Tak, przypuszczam, że masz rację ... Chociaż mogę po prostu powrócić do stanowiska, że ​​API C nie jest częścią Pythona . Jest częścią Cpython :-P
mgilson
67

Pod przykryciem i += 1robi coś takiego:

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

Chociaż i = i + 1robi coś takiego:

i = i.__add__(1)

Jest to niewielkie nadmierne uproszczenie, ale pojawia się pomysł: Python daje rodzajom specjalne sposoby obsługi +=, tworząc zarówno __iadd__metodę, jak i metodę __add__.

Chodzi o to, że typy zmienne, takie jak list, mutują się __iadd__(a następnie wracają self, chyba że robisz coś bardzo trudnego), podczas gdy typy niezmienne, takie jakint , po prostu tego nie implementują.

Na przykład:

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

Ponieważ l2jest to ten sam obiekt, co l1i zmutowaliście l1, zmutowaliście równieżl2 .

Ale:

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

Tutaj nie mutowałeś l1; zamiast tego utworzyłeś nową listę l1 + [3]i odbij nazwę, l1aby na nią wskazywać, pozostawiającl2 wskazywanie na oryginalnej liście.

(W +=wersji ponownie wiązałeś l1, po prostu w takim przypadku wiązałeś go z tym samym, z listktórym był już związany, więc zwykle możesz zignorować tę część).

abarnert
źródło
czy __iadd__faktycznie zadzwonić __add__w razie AttributeError?
mgilson
Cóż, i.__iadd__nie dzwoni __add__; to i += 1to wzywa __add__.
abarnert
errr ... Tak, o to mi chodziło. Ciekawy. Nie wiedziałem, że zrobiono to automatycznie.
mgilson
3
Pierwsza próba jest w rzeczywistości i = i.__iadd__(1)- iadd może zmodyfikować obiekt w miejscu, ale nie musi, więc oczekuje się, że zwróci wynik w obu przypadkach.
lvc
Należy pamiętać, że oznacza to, że operator.iaddrozmowy __add__na temat AttributeError, ale nie może on ponownie powiązać wynik ... więc i=1; operator.iadd(i, 1)zwraca 2 i liści iustawiona 1. Co jest nieco mylące.
abarnert
6

Oto przykład, który bezpośrednio porównuje i += xz 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]
Deqing
źródło