Dlaczego + = zachowuje się nieoczekiwanie na listach?


118

+=Operator w python zdaje się nieoczekiwanie działa na listach. Czy ktoś może mi powiedzieć, co się tutaj dzieje?

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

WYNIK

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += barwydaje się wpływać na każdą instancję klasy, podczas gdy foo = foo + barwydaje się , że zachowuje się w sposób, jakiego oczekiwałbym od rzeczy.

+=Operatora nazywana jest „związek operatorowi przypisanie”.


zobacz także różnicę między „rozszerzeniem” i „dołączeniem” na liście
N 1.1

3
Nie sądzę, żeby to wskazywało na coś złego w Pythonie. Większość języków nie pozwoliłaby nawet na użycie +operatora na tablicach. Myślę, że w tym przypadku ma to sens +=.
Skilldrick,

4
Oficjalnie nazywa się to „zadaniem rozszerzonym”.
Martijn Pieters

Odpowiedzi:


138

Ogólna odpowiedź jest taka +=, że próbuje wywołać __iadd__metodę specjalną, a jeśli ta nie jest dostępna, próbuje użyć __add__zamiast niej. Tak więc problem polega na różnicy między tymi specjalnymi metodami.

__iadd__Specjalna metoda jest dla dodatku w miejscu, to jest to obiekt, który mutuje działa dalej. __add__Szczególny sposób powraca nowego przedmiotu i jest wykorzystywany do standardowej +operatora.

Więc kiedy +=operator jest używany na obiekcie, który ma __iadd__zdefiniowane, obiekt jest modyfikowany w miejscu. W przeciwnym razie spróbuje użyć zwykłego __add__i zwróci nowy obiekt.

Dlatego dla typów zmiennych, takich jak listy, +=zmienia się wartość obiektu, podczas gdy dla typów niezmiennych, takich jak krotki, ciągi znaków i liczby całkowite, zamiast tego zwracany jest nowy obiekt ( a += bstaje się równoważny a = a + b).

Dla typów że wsparcie zarówno __iadd__i __add__ty dlatego trzeba być ostrożnym, który z nich korzystać. a += bbędzie dzwonić __iadd__i mutować a, podczas gdy a = a + bstworzy nowy obiekt i przypisze go do a. To nie ta sama operacja!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

Dla typów niezmiennych (gdzie nie masz __iadd__) a += bi a = a + bsą równoważne. To właśnie pozwala ci używać +=na niezmiennych typach, co może wydawać się dziwną decyzją projektową, dopóki nie uznasz, że w przeciwnym razie nie możesz użyć +=na niezmiennych typach, takich jak liczby!


4
Istnieje również __radd__metoda, która może być czasami wywoływana (ma to znaczenie dla wyrażeń, które obejmują głównie podklasy).
jfs

2
Z perspektywy: + = jest przydatne, jeśli ważna jest pamięć i szybkość
Norfeldt

3
Wiedząc, że +=faktycznie rozszerza listę, to wyjaśnia, dlaczego x = []; x = x + {}daje TypeErrorchwilę x = []; x += {}po prostu wraca [].
zezollo

96

W ogólnym przypadku, zobacz odpowiedź Scotta Griffitha . Jednak w przypadku list takich jak ty +=operator jest skrótem od someListObject.extend(iterableObject). Zobacz dokumentację rozszerzenia () .

extendFunkcja dołączania wszystkich elementów parametr do listy.

Kiedy foo += somethingmodyfikujesz listę foow miejscu, nie zmieniasz odniesienia, na które foowskazuje nazwa , ale bezpośrednio zmieniasz obiekt listy. W foo = foo + somethingrzeczywistości tworzysz nową listę.

Ten przykładowy kod wyjaśni to:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

Zwróć uwagę, jak zmienia się odniesienie po ponownym przypisaniu nowej listy do l.

Ponieważ barjest to zmienna klasy, a nie zmienna instancji, modyfikacja lokalna wpłynie na wszystkie instancje tej klasy. Jednak podczas redefiniowania self.barinstancja będzie miała oddzielną zmienną instancji self.barbez wpływu na inne instancje klas.


7
Nie zawsze jest to prawdą: a = 1; a + = 1; jest poprawnym Pythonem, ale int nie mają żadnych metod „rozszerzania ()”. Nie możesz tego uogólniać.
e-satis

2
Zrobiłem kilka testów, Scott Griffiths zrobił to dobrze, więc -1 dla ciebie.
e-satis

11
@ e-statis: PO wyraźnie mówił o listach, a ja wyraźnie stwierdziłem, że mówię też o listach. Niczego nie uogólniam.
AndiDog

Usunięto -1, odpowiedź jest wystarczająco dobra. Nadal uważam, że odpowiedź Griffithsa jest lepsza.
e-satis,

Na początku dziwne jest myślenie, że a += bróżni się to od a = a + bdwóch list ai b. Ale to ma sens; extendczęściej będzie to zamierzone do zrobienia z listami, zamiast tworzyć nową kopię całej listy, która będzie miała większą złożoność czasową. Jeśli programiści muszą uważać, aby nie modyfikować oryginalnych list na miejscu, krotki są lepszą opcją jako niezmienne obiekty. +=z krotkami nie może modyfikować oryginalnej krotki.
Pranjal Mittal

22

Problem polega na tym, że barjest definiowany jako atrybut klasy, a nie zmienna instancji.

W fooprogramie atrybut klasy jest modyfikowany w initmetodzie, dlatego dotyczy to wszystkich instancji.

W programie foo2zmienna instancji jest definiowana przy użyciu (pustego) atrybutu klasy, a każda instancja otrzymuje własną bar.

„Prawidłowa” implementacja to:

class foo:
    def __init__(self, x):
        self.bar = [x]

Oczywiście atrybuty klas są całkowicie legalne. W rzeczywistości możesz uzyskać do nich dostęp i modyfikować je bez tworzenia instancji klasy w następujący sposób:

class foo:
    bar = []

foo.bar = [x]

8

W grę wchodzą dwie rzeczy:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+operator wywołuje __add__metodę z listy. Pobiera wszystkie elementy ze swoich operandów i tworzy nową listę zawierającą te elementy, zachowując ich kolejność.

+=__iadd__metoda wywołania operatora na liście. Pobiera iterowalne i dołącza wszystkie elementy iterowalne do listy w miejscu. Nie tworzy nowego obiektu listy.

W klasie fooinstrukcja self.bar += [x]nie jest instrukcją przypisania, ale w rzeczywistości jest tłumaczeniem

self.bar.__iadd__([x])  # modifies the class attribute  

który modyfikuje listę w miejscu i działa jak metoda list extend.

W klasie foo2wręcz przeciwnie, instrukcja przypisania w initmetodzie

self.bar = self.bar + [x]  

można zdekonstruować jako:
Instancja nie ma atrybutu bar(istnieje jednak atrybut klasy o tej samej nazwie), więc uzyskuje dostęp do atrybutu klasy bari tworzy nową listę, dołączając xdo niej. Oświadczenie przekłada się na:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

Następnie tworzy atrybut instancji bari przypisuje do niego nowo utworzoną listę. Zauważ, że barprawa strona zadania różni się od barprawej strony.

Dla instancji klasy foo, barjest atrybutem klasy i nie atrybut instancji. W związku z tym każda zmiana atrybutu klasy barzostanie odzwierciedlona we wszystkich instancjach.

Wręcz przeciwnie, każda instancja klasy foo2ma swój własny atrybut instancji, barktóry różni się od atrybutu klasy o tej samej nazwie bar.

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

Mam nadzieję, że to wszystko wyjaśnia.


5

Chociaż minęło dużo czasu i powiedziano wiele poprawnych rzeczy, nie ma odpowiedzi, która łączy oba efekty.

Masz 2 efekty:

  1. „specjalne”, być może niezauważone zachowanie list z +=(jak stwierdził Scott Griffiths )
  2. fakt, że w grę wchodzą atrybuty klas, a także atrybuty instancji (jak stwierdził Can Berk Büder )

W klasie fooThe __init__metoda modyfikuje atrybutu class. To dlatego, że self.bar += [x]przekłada się na self.bar = self.bar.__iadd__([x]). __iadd__()służy do modyfikacji w miejscu, więc modyfikuje listę i zwraca do niej odniesienie.

Należy zauważyć, że dyktando instancji jest modyfikowane, chociaż normalnie nie byłoby to konieczne, ponieważ dyktando klasy zawiera już to samo przypisanie. Więc ten szczegół pozostaje prawie niezauważony - z wyjątkiem sytuacji, gdy zrobisz to foo.bar = []później. Tutaj instancje barpozostają niezmienione dzięki wspomnianemu faktowi.

W klasie foo2, jednak klasa użytkownika barjest używana, ale nie dotknął. Zamiast tego [x]dodawane jest do niego a, tworząc nowy obiekt, jak self.bar.__add__([x])nazywamy go tutaj, który nie modyfikuje obiektu. Wynik jest następnie umieszczany w dict instancji, dając instancji nową listę jako dict, podczas gdy atrybut klasy pozostaje zmodyfikowany.

Rozróżnienie między ... = ... + ...i ... += ...dotyczy również późniejszych zadań:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 

Możesz zweryfikować tożsamość obiektów za pomocą print id(foo), id(f), id(g)(nie zapomnij o dodatkowych ()s, jeśli korzystasz z Python3).

BTW: +=Operator jest nazywany „przypisaniem rozszerzonym” i ogólnie jest przeznaczony do wykonywania modyfikacji w miejscu, o ile to możliwe.


5

Wydaje się, że inne odpowiedzi w dużej mierze ją obejmują, chociaż wydaje się, że warto zacytować i odnieść się do Rozszerzonego Zadania PEP 203 :

[zwiększonych operatory przypisania] wdrożenia tego samego operatora, normalnej postaci binarnej, z tym, że operacja jest wykonywana `w miejscu”, gdy lewy podparcia obiektu strony jest, a po lewej stronie jest obliczane tylko raz.

...

Ideą przypisania rozszerzonego w Pythonie jest to, że nie jest to tylko łatwiejszy sposób zapisania powszechnej praktyki przechowywania wyniku operacji binarnej w jej operandzie po lewej stronie, ale także sposób na to, aby dany operand po lewej stronie Wiedz, że powinien działać „na sobie”, zamiast tworzyć zmodyfikowaną kopię siebie.


1
>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])

0
>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

Widzimy, że kiedy próbujemy zmodyfikować niezmienny obiekt (w tym przypadku liczbę całkowitą), Python po prostu daje nam inny obiekt. Z drugiej strony jesteśmy w stanie wprowadzić zmiany w zmiennym obiekcie (liście) i pozostawić go tym samym obiektem przez cały czas.

ref: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

Zapoznaj się również z poniższym adresem URL, aby zrozumieć płytką i głęboką kopię

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/


# ID jest takie samo dla list
roshan ok
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.