+ = Neden listelerde beklenmedik şekilde davranıyor?


118

+=Python operatör listelerinde beklenmedik işletim gibi görünüyor. Biri bana burada neler olduğunu söyleyebilir mi?

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 

ÇIKTI

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

foo += barGörünüşe göre sınıfın her örneğini etkiliyor gibi görünürken, foo = foo + barşeylerden beklediğim şekilde davranıyor gibi görünüyor.

+=Operatör, bir "bileşik atama operatör" olarak adlandırılır.


Listede 'uzatma' ve 'ekleme' arasındaki farkı da görün
N 1.1

3
Bunun Python'da yanlış bir şey gösterdiğini sanmıyorum. Çoğu dil, +operatörü dizilerde kullanmanıza bile izin vermez . Bence bu durumda son derece mantıklı geliyor +=.
Skilldrick

4
Resmi olarak buna 'artırılmış atama' denir.
Martijn Pieters

Yanıtlar:


138

Genel cevap, özel yöntemi +=çağırmaya çalışmasıdır __iadd__ve bu mevcut değilse, __add__onun yerine kullanmaya çalışır . Yani sorun, bu özel yöntemler arasındaki farkla ilgilidir.

__iadd__Özel yöntem mutasyon göre hareket bu nesne olduğunu, bir yerinde ek içindir. __add__Özel bir yöntem, yeni bir nesne döner ve aynı zamanda standart için kullanılan +operatör.

Dolayısıyla, +=operatör __iadd__tanımlanmış bir nesne üzerinde kullanıldığında , nesne yerinde değiştirilir. Aksi takdirde, düzlüğü kullanmaya __add__ve yeni bir nesne döndürmeye çalışacaktır .

Listeler gibi değişken türler +=için nesnenin değerini değiştirirken, tuples, dizeler ve tamsayılar gibi değişmez türler için bunun yerine yeni bir nesne döndürülür ( a += beşdeğer olur a = a + b).

Türleri için desteğin hem __iadd__ve __add__bu nedenle kullandığınız hangisinin dikkatli olmak zorunda. a += bçağıracak __iadd__ve değişime uğrayacak a, oysa a = a + byeni bir nesne oluşturacak ve ona atayacaktır a. Aynı operasyon değiller!

>>> 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

Değişmez türler için (anınızın olmadığı durumlarda __iadd__) a += bve a = a + beşdeğerdir. Bu, +=değişmez türlerde kullanmanıza izin veren şeydir , bu, aksi takdirde +=sayılar gibi değişmez türler üzerinde kullanamayacağınızı düşünene kadar garip bir tasarım kararı gibi görünebilir !


4
__radd__Bazen çağrılabilecek bir yöntem de vardır (çoğunlukla alt sınıfları içeren ifadelerle ilgilidir).
jfs

2
Perspektif olarak: + =, hafıza ve hız önemliyse kullanışlıdır
Norfeldt

3
Bunun +=aslında bir listeyi genişlettiğini bilerek , bu neden x = []; x = x + {}bir TypeErrorsüre x = []; x += {}sadece geri döndüğünü açıklıyor [].
zezollo

96

Genel durum için, Scott Griffith'in cevabına bakın . Sizin gibi listelerle uğraşırken, yine de +=operatör için bir kısaltmadır someListObject.extend(iterableObject). Ext () belgelerine bakın .

extendFonksiyon listesine parametrenin tüm unsurları ekler.

Bunu yaparken foo += somethinglisteyi fooyerinde değiştirirsiniz , böylece adın fooişaret ettiği referansı değiştirmezsiniz , ancak liste nesnesini doğrudan değiştirirsiniz. İle foo = foo + somethingaslında yeni bir liste oluşturuyorsunuz.

Bu örnek kod bunu açıklayacaktır:

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

Yeni listeyi yeniden atadığınızda referansın nasıl değiştiğine dikkat edin l.

barBir örnek değişkeni yerine bir sınıf değişkeni olduğu gibi , yerinde değişiklik yapmak o sınıfın tüm örneklerini etkileyecektir. Ancak yeniden tanımlanırken self.bar, örnek self.bardiğer sınıf örneklerini etkilemeden ayrı bir örnek değişkenine sahip olacaktır .


7
Bu her zaman doğru değildir: a = 1; a + = 1; geçerli bir Python'dur, ancak ints'in herhangi bir "extended ()" yöntemi yoktur. Bunu genelleyemezsiniz.
e-satis

2
Bazı testler yaptım, Scott Griffiths doğru anladı, yani sizin için -1.
e-satis

11
@ e-statis: OP açıkça listelerden bahsediyordu ve ben de listelerden bahsettiğimi açıkça belirttim. Ben hiçbir şeyi genellemiyorum.
AndiDog

-1 kaldırıldı, cevap yeterli. Yine de Griffiths'in cevabının daha iyi olduğunu düşünüyorum.
e-satis

İlk başta bunun iki listeden a += bfarklı olduğunu düşünmek garip geliyor ve . Ama mantıklı; daha yüksek zaman karmaşıklığına sahip olan tüm listenin yeni bir kopyasını oluşturmaktan çok, listelerle yapılması amaçlanan şey olacaktır. Geliştiricilerin orijinal listeleri yerinde değiştirmemelerine dikkat etmeleri gerekiyorsa, o zaman demetler, değişmez nesneler olarak daha iyi bir seçenektir. demetler ile orijinal demeti değiştiremez. a = a + babextend+=
Pranjal Mittal

22

Buradaki sorun bar, bir örnek değişkeni değil, bir sınıf özelliği olarak tanımlanmasıdır.

İçinde foo, sınıf özniteliği inityöntemde değiştirilir , bu nedenle tüm örnekler etkilenir.

İçinde foo2, bir örnek değişkeni (boş) sınıf özniteliği kullanılarak tanımlanır ve her örnek kendine ait olur bar.

"Doğru" uygulama şu şekilde olacaktır:

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

Elbette sınıf özellikleri tamamen yasaldır. Aslında, aşağıdaki gibi bir sınıf örneği oluşturmadan bunlara erişebilir ve bunları değiştirebilirsiniz:

class foo:
    bar = []

foo.bar = [x]

8

Burada iki şey söz konusudur:

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

+operatör __add__yöntemi bir listede çağırır . Tüm öğeleri işlenenlerinden alır ve sıralarını koruyan bu öğeleri içeren yeni bir liste oluşturur.

+=operatör __iadd__listedeki yöntemi çağırır . Bir yinelenebilir alır ve yinelenebilirin tüm öğelerini yerinde listeye ekler. Yeni bir liste nesnesi oluşturmaz.

Sınıfta fooifade self.bar += [x]bir atama ifadesi değildir, ancak aslında

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

listeyi yerinde değiştiren ve liste yöntemi gibi davranan extend.

Sınıfta foo2, aksine, inityöntemdeki atama ifadesi

self.bar = self.bar + [x]  

şu şekilde yeniden yapılandırılabilir:
Örneğin, özniteliği yoktur bar(yine de aynı ada sahip bir sınıf özniteliği vardır), bu nedenle sınıf özniteliğine erişir ve ona barekleyerek yeni bir liste oluşturur x. İfade şu anlama gelir:

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

Daha sonra bir örnek niteliği baroluşturur ve yeni oluşturulan listeyi buna atar. barAtamanın rhs'sinin lhs'dekinden farklı olduğuna dikkat edin bar.

Sınıfın örnekleri için foo, barbir sınıf niteliği değil, örnek özelliğidir. Bu nedenle, sınıf özelliğindeki herhangi bir değişiklik bartüm örnekler için yansıtılacaktır.

Aksine, sınıfın her bir örneği , aynı adın sınıf özelliğinden farklı olan foo2kendi örnek niteliğine barsahiptir bar.

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

Umarım bu işleri temizler.


5

Çok zaman geçmesine ve pek çok doğru şey söylendiği halde, her iki etkiyi bir araya getiren bir cevap yoktur.

2 etkiniz var:

  1. listelerin "özel", belki de fark edilmeyen davranışı +=( Scott Griffiths tarafından belirtildiği gibi )
  2. sınıf yanı (belirttiği gibi örnek nitelikleri katılmaktadırlar olarak niteliklerini gerçeği Can Berk BUDER )

Sınıfta foo, __init__yöntem sınıf özniteliğini değiştirir. Çünkü self.bar += [x]tercüme edilir self.bar = self.bar.__iadd__([x]). __iadd__()yerinde değişiklik içindir, bu nedenle listeyi değiştirir ve ona bir referans döndürür.

Örnek diktenin değiştirildiğine dikkat edin, ancak sınıf diktesi zaten aynı atamayı içerdiğinden bu normalde gerekli değildir. Yani bu ayrıntı neredeyse fark edilmeyecek - foo.bar = []daha sonra bir ödül yapmadığınız sürece . Burada barsöz konusu gerçek sayesinde örnekler aynı kalır.

Ancak sınıfta foo2, sınıfınki barkullanılır, ancak dokunulmaz. Bunun yerine, burada denildiği [x]gibi self.bar.__add__([x]), nesneyi değiştirmeyen yeni bir nesne oluşturarak ona eklenir . Sonuç, daha sonra örnek dikteye yerleştirilir ve örneğe yeni liste bir dikte verilirken, sınıfın özniteliği değiştirilmiş olarak kalır.

Aradaki ayrım ... = ... + ...ve ... += ...daha sonraki ödevleri etkiler:

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 

İle nesnelerin kimliğini doğrulayabilirsiniz print id(foo), id(f), id(g)( ()Python3 üzerindeyseniz ek URL'leri unutmayın).

BTW: +=Operatör "artırılmış atama" olarak adlandırılır ve genellikle mümkün olduğunca yerinde değişiklikler yapması amaçlanır.


5

Diğer cevaplar, Artırılmış Ödevler KEP 203'ten alıntı yapmaya ve atıfta bulunmaya değer görünse de, hemen hemen kapsıyor gibi görünüyor :

Onlar [artırılmış atama operatörleri] , işlemin sol taraftaki nesne tarafından desteklendiğinde `` yerinde '' yapılması ve sol tarafın yalnızca bir kez değerlendirilmesinin dışında, normal ikili biçimleriyle aynı operatörü uygularlar.

...

Python'da artırılmış atamanın arkasındaki fikir, ikili işlemin sonucunu sol el operandında saklamanın genel uygulamasını yazmanın daha kolay bir yolu değil, aynı zamanda söz konusu sol el operandının kendisinin değiştirilmiş bir kopyasını oluşturmak yerine, `` kendi başına '' çalışması gerektiğini bilir.


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

Değişmez bir nesneyi (bu durumda tam sayı) değiştirmeye çalıştığımızda, Python'un bize bunun yerine basitçe farklı bir nesne verdiğini görürüz. Öte yandan, değiştirilebilir bir nesnede (bir liste) değişiklik yapabilir ve baştan sona aynı nesne olarak kalmasını sağlayabiliriz.

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

Ayrıca yüzeysel kopya ve derin kopyayı anlamak için aşağıdaki url'ye bakın

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


# ID Listeler için aynı
roshan ok
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.