Mengapa + = berperilaku tidak terduga di daftar?

118

The +=operator dalam python tampaknya beroperasi tiba-tiba pada daftar. Adakah yang bisa memberi tahu saya apa yang terjadi di sini?

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 

KELUARAN

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

foo += bartampaknya memengaruhi setiap contoh kelas, sedangkan foo = foo + bartampaknya berperilaku dengan cara yang saya harapkan.

The +=operator disebut "senyawa tugas operator".

eucalculia.dll
sumber
lihat perbedaan antara 'perpanjang' dan 'tambahkan' pada daftar juga
N 1,1
3
Saya tidak berpikir ini menunjukkan sesuatu yang salah dengan Python. Sebagian besar bahasa bahkan tidak mengizinkan Anda menggunakan +operator pada array. Saya pikir itu masuk akal dalam kasus ini yang +=akan ditambahkan.
Skilldrick
4
Ini disebut 'tugas tambahan', secara resmi.
Martijn Pieters

Jawaban:

138

Jawaban umumnya adalah +=mencoba memanggil __iadd__metode khusus, dan jika itu tidak tersedia, ia mencoba menggunakan __add__sebagai gantinya. Jadi masalahnya ada pada perbedaan antara metode khusus ini.

The __iadd__Metode khusus adalah untuk tambahan di tempat, yang itu bermutasi objek yang ia bertindak atas. The __add__metode khusus mengembalikan objek baru dan juga digunakan untuk standar +operator.

Jadi, ketika +=operator digunakan pada objek yang telah __iadd__ditentukan, objek dimodifikasi di tempat. Jika tidak, ia malah akan mencoba menggunakan dataran __add__dan mengembalikan objek baru.

Itulah mengapa untuk tipe yang bisa berubah seperti daftar +=mengubah nilai objek, sedangkan untuk tipe yang tidak bisa diubah seperti tupel, string dan integer, objek baru dikembalikan sebagai gantinya ( a += bmenjadi setara dengan a = a + b).

Untuk tipe yang mendukung keduanya __iadd__dan __add__oleh karena itu Anda harus berhati-hati yang mana yang Anda gunakan. a += bakan memanggil __iadd__dan bermutasi a, sedangkan a = a + bakan membuat objek baru dan menetapkannya a. Mereka bukanlah operasi yang sama!

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

Untuk tipe yang tidak dapat diubah (jika Anda tidak memiliki __iadd__) a += bdan a = a + byang setara. Inilah yang memungkinkan Anda menggunakan +=tipe yang tidak dapat diubah, yang mungkin tampak seperti keputusan desain yang aneh sampai Anda mempertimbangkannya jika tidak, Anda tidak dapat menggunakannya +=pada tipe yang tidak dapat diubah seperti angka!

Scott Griffiths
sumber
4
Ada juga __radd__metode yang terkadang dipanggil (ini relevan untuk ekspresi yang melibatkan sebagian besar subclass).
jfs
2
Dalam perspektif: + = berguna jika memori dan kecepatan penting
Norfeldt
3
Mengetahui bahwa +=sebenarnya memperluas daftar, ini menjelaskan mengapa x = []; x = x + {}memberi TypeErrorwaktu x = []; x += {}baru saja kembali [].
zezollo
96

Untuk kasus umum, lihat jawaban Scott Griffith . Namun, ketika berurusan dengan daftar seperti Anda, +=operator adalah singkatan dari someListObject.extend(iterableObject). Lihat dokumentasi extender () .

The extendfungsi akan menambahkan semua elemen parameter ke dalam daftar.

Saat melakukan foo += somethingAnda memodifikasi daftar foodi tempat, sehingga Anda tidak mengubah referensi yang ditunjuk oleh nama foo, tetapi Anda mengubah objek daftar secara langsung. Dengan foo = foo + something, Anda sebenarnya sedang membuat daftar baru .

Kode contoh ini akan menjelaskannya:

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

Perhatikan bagaimana referensi berubah saat Anda menetapkan kembali daftar baru l.

Sebagai barvariabel kelas dan bukan variabel instan, memodifikasi di tempat akan mempengaruhi semua instance kelas itu. Namun saat mendefinisikan ulang self.bar, instance akan memiliki variabel instance terpisah self.bartanpa memengaruhi instance kelas lainnya.

AndiDog
sumber
7
Ini tidak selalu benar: a = 1; a + = 1; adalah Python yang valid, tetapi int tidak memiliki metode "expand ()". Anda tidak dapat menggeneralisasi ini.
e-satis
2
Melakukan beberapa pengujian, Scott Griffiths melakukannya dengan benar, jadi -1 untuk Anda.
e-satis
11
@ e-statistik: OP berbicara dengan jelas tentang daftar, dan saya dengan jelas menyatakan bahwa saya juga berbicara tentang daftar. Saya tidak menggeneralisasi apa pun.
AndiDog
Menghapus -1, jawabannya cukup baik. Saya masih berpikir jawaban Griffiths lebih baik.
e-satis
Pada awalnya terasa aneh untuk berpikir bahwa a += bitu berbeda dari a = a + buntuk dua daftar adan b. Tapi itu masuk akal; extendlebih sering menjadi hal yang dimaksudkan untuk dilakukan dengan daftar daripada membuat salinan baru dari seluruh daftar yang akan memiliki kompleksitas waktu yang lebih tinggi. Jika pengembang perlu berhati-hati agar mereka tidak mengubah daftar asli pada tempatnya, maka tupel adalah pilihan yang lebih baik sebagai objek yang tidak dapat diubah. +=dengan tupel tidak dapat memodifikasi tupel asli.
Pranjal Mittal
22

Masalahnya di sini adalah, bardidefinisikan sebagai atribut kelas, bukan variabel instan.

Di foo, atribut class diubah dalam initmetode, itulah sebabnya semua instance terpengaruh.

Dalam foo2, variabel instance didefinisikan menggunakan atribut kelas (kosong), dan setiap instance mendapatkan miliknya sendiri bar.

Penerapan yang "benar" akan menjadi:

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

Tentu saja, atribut kelas sepenuhnya legal. Faktanya, Anda dapat mengakses dan memodifikasinya tanpa membuat instance kelas seperti ini:

class foo:
    bar = []

foo.bar = [x]
Can Berk Güder
sumber
8

Ada dua hal yang terlibat di sini:

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

+operator memanggil __add__metode tersebut pada daftar. Ini mengambil semua elemen dari operannya dan membuat daftar baru yang berisi elemen-elemen yang menjaga urutannya.

+=__iadd__metode panggilan operator pada daftar. Dibutuhkan iterable dan menambahkan semua elemen dari iterable ke daftar di tempatnya. Itu tidak membuat objek daftar baru.

Di kelas foopernyataan self.bar += [x]tersebut bukanlah pernyataan tugas tetapi sebenarnya diterjemahkan menjadi

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

yang mengubah daftar di tempat dan bertindak seperti metode daftar extend.

Sebaliknya foo2, di kelas , pernyataan tugas dalam initmetode

self.bar = self.bar + [x]  

dapat didekonstruksi sebagai:
Instance ini tidak memiliki atribut bar(meskipun demikian, ada atribut kelas dengan nama yang sama) sehingga ia mengakses atribut kelas bardan membuat daftar baru dengan menambahkannya x. Pernyataan itu diterjemahkan menjadi:

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

Kemudian itu membuat atribut instance bardan menetapkan daftar yang baru dibuat ke dalamnya. Perhatikan bahwa bardi rhs tugas berbeda dari bardi lhs.

Untuk contoh kelas foo, baradalah atribut kelas dan bukan atribut contoh. Karenanya setiap perubahan pada atribut kelas barakan tercermin untuk semua contoh.

Sebaliknya, setiap instance kelas foo2memiliki atribut instance sendiri-sendiri baryang berbeda dengan atribut kelas yang bernama sama bar.

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

Semoga ini membereskan semuanya.

ajay
sumber
5

Meskipun banyak waktu telah berlalu dan banyak hal yang benar telah dikatakan, tidak ada jawaban yang menggabungkan kedua efek tersebut.

Anda memiliki 2 efek:

  1. perilaku "khusus", mungkin tanpa disadari dari daftar dengan +=(seperti yang dinyatakan oleh Scott Griffiths )
  2. fakta bahwa atribut kelas serta atribut contoh terlibat (seperti yang dinyatakan oleh Can Berk Büder )

Di kelas foo, __init__metode memodifikasi atribut kelas. Itu karena self.bar += [x]diterjemahkan menjadi self.bar = self.bar.__iadd__([x]). __iadd__()adalah untuk modifikasi di tempat, sehingga memodifikasi daftar dan mengembalikan referensi ke sana.

Perhatikan bahwa instance dict diubah meskipun ini biasanya tidak diperlukan karena class dict sudah berisi tugas yang sama. Jadi detail ini hampir tidak diperhatikan - kecuali jika Anda melakukannya foo.bar = []setelahnya. Di sini kasusnya bartetap sama berkat fakta yang disebutkan.

Di kelas foo2, bagaimanapun, kelas bardigunakan, tetapi tidak disentuh. Sebagai gantinya, a [x]ditambahkan padanya, membentuk objek baru, seperti self.bar.__add__([x])yang disebut di sini, yang tidak memodifikasi objek. Hasilnya dimasukkan ke dalam instance dict, lalu memberi instance daftar baru sebagai dict, sementara atribut kelas tetap dimodifikasi.

Perbedaan antara ... = ... + ...dan juga ... += ...mempengaruhi tugas setelahnya:

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 

Anda dapat memverifikasi identitas objek dengan print id(foo), id(f), id(g)(jangan lupa tambahan ()jika Anda menggunakan Python3).

BTW: +=Operator disebut "penugasan tambahan" dan umumnya dimaksudkan untuk melakukan modifikasi di tempat sejauh mungkin.

glglgl
sumber
5

Jawaban lain sepertinya sudah cukup banyak, meskipun tampaknya layak dikutip dan mengacu pada Tugas Tertambah PEP 203 :

Mereka [operator penugasan tambahan] mengimplementasikan operator yang sama seperti bentuk biner normalnya, kecuali bahwa operasi tersebut dilakukan 'di tempat' saat objek sisi kiri mendukungnya, dan sisi kiri hanya dievaluasi sekali.

...

Gagasan di balik penugasan tambahan dengan Python adalah bahwa ini bukan hanya cara yang lebih mudah untuk menulis praktik umum menyimpan hasil operasi biner di operan kiri, tetapi juga cara untuk operan kiri yang dimaksud untuk Ketahuilah bahwa itu harus beroperasi 'sendiri', daripada membuat salinan dirinya yang dimodifikasi.

mwardm
sumber
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])
kusut
sumber
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

Kami melihat bahwa ketika kami mencoba untuk memodifikasi objek yang tidak dapat diubah (bilangan bulat dalam kasus ini), Python hanya memberi kami objek yang berbeda. Di sisi lain, kita dapat membuat perubahan pada objek yang bisa berubah (daftar) dan membuatnya tetap menjadi objek yang sama.

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

Lihat juga url di bawah untuk memahami shallowcopy dan deepcopy

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

roshan ok
sumber
# ID sama untuk List
roshan ok