Kapan "i + = x" berbeda dari "i = i + x" dengan Python?

212

Saya diberitahu bahwa +=dapat memiliki efek yang berbeda dari notasi standar i = i +. Apakah ada kasus yang i += 1berbeda i = i + 1?

MarJamRob
sumber
7
+=bertindak seperti extend()dalam kasus daftar.
Ashwini Chaudhary
12
@AshwiniChaudhary Itu perbedaan yang cukup halus, mengingat i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]ini True. Banyak pengembang mungkin tidak memperhatikan bahwa id(i)perubahan untuk satu operasi, tetapi tidak yang lain.
kojiro
1
@ Kojiro - Meskipun ini adalah perbedaan yang halus, saya pikir ini adalah yang penting.
mgilson
@ Mcgson itu penting, jadi saya merasa perlu penjelasan. :)
kojiro
1
Pertanyaan terkait tentang perbedaan antara keduanya di Jawa: stackoverflow.com/a/7456548/245966
jakub.g

Jawaban:

317

Ini sepenuhnya tergantung pada objek i.

+=memanggil __iadd__metode (jika ada - mundur kembali __add__jika tidak ada) sedangkan +memanggil __add__metode 1 atau __radd__metode dalam beberapa kasus 2 .

Dari perspektif API, __iadd__seharusnya digunakan untuk memodifikasi objek yang bisa berubah di tempat (mengembalikan objek yang dimutasi) sedangkan __add__seharusnya mengembalikan contoh baru dari sesuatu. Untuk objek yang tidak dapat diubah , kedua metode mengembalikan instance baru, tetapi __iadd__akan menempatkan instance baru di namespace saat ini dengan nama yang sama dengan instance lama. Ini sebabnya

i = 1
i += 1

tampaknya meningkat i. Pada kenyataannya, Anda mendapatkan integer baru dan menetapkannya "di atas" i- kehilangan satu referensi ke integer lama. Dalam hal ini, i += 1persis sama dengan i = i + 1. Tapi, dengan sebagian besar objek yang bisa berubah, ini adalah cerita yang berbeda:

Sebagai contoh nyata:

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]

dibandingkan dengan:

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

pemberitahuan bagaimana dalam contoh pertama, karena bdan areferensi objek yang sama, ketika saya menggunakan +=pada b, itu benar-benar berubah b(dan amelihat perubahan itu juga - Setelah semua, itu referensi daftar yang sama). Namun dalam kasus kedua, ketika saya melakukannya b = b + [1, 2, 3], ini mengambil daftar yang bmereferensikan dan menyatukannya dengan daftar baru [1, 2, 3]. Itu kemudian menyimpan daftar gabungan di namespace saat ini sebagai b- Tanpa memperhatikan apa bbaris sebelumnya.


1 Dalam ekspresi x + y, jika x.__add__tidak diimplementasikan atau jika x.__add__(y)kembali NotImplemented dan xdan ymemiliki jenis yang berbeda , maka x + ycobalah menelepon y.__radd__(x). Jadi, dalam kasus di mana Anda miliki

foo_instance += bar_instance

jika Footidak menerapkan __add__atau __iadd__hasilnya di sini sama dengan

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2 Dalam ekspresi foo_instance + bar_instance, bar_instance.__radd__akan dicoba sebelumnya foo_instance.__add__ jika jenisnya bar_instanceadalah subkelas dari jenis foo_instance(misalnya issubclass(Bar, Foo)). Rasional untuk ini adalah karena Bardalam beberapa arti "-tingkat yang lebih tinggi" objek dari Foosehingga Barharus mendapatkan pilihan utama Fooperilaku 's.

mgilson
sumber
18
Nah, +=panggilan __iadd__ jika ada , dan kembali ke menambah dan membatalkan sebaliknya. Itu sebabnya i = 1; i += 1bekerja meskipun tidak ada int.__iadd__. Tapi selain nit kecil itu, penjelasannya bagus sekali.
abarnert
4
@abarnert - Saya selalu berasumsi bahwa int.__iadd__baru saja menelepon __add__. Saya senang telah mempelajari sesuatu yang baru hari ini :).
mgilson
@abarnert - Saya kira mungkin harus lengkap , x + ypanggilan y.__radd__(x)jika x.__add__tidak ada (atau kembali NotImplementeddan xdan ydari jenis yang berbeda)
mgilson
Jika Anda benar-benar ingin menjadi pelengkap, Anda harus menyebutkan bahwa bit "jika ada" melewati mekanisme getattr yang biasa, kecuali untuk beberapa quirks dengan kelas klasik, dan untuk tipe yang diimplementasikan dalam C API ia malah mencari baik nb_inplace_addatau sq_inplace_concat, dan fungsi-fungsi C API memiliki persyaratan yang lebih ketat daripada metode Python dunder, dan ... Tapi saya tidak berpikir itu relevan dengan jawabannya. Perbedaan utamanya adalah +=mencoba melakukan penambahan di tempat sebelum kembali bertindak seperti +, yang menurut saya sudah Anda jelaskan.
abarnert
Ya, saya kira Anda benar ... Meskipun saya bisa saja kembali pada pendirian bahwa C API bukan bagian dari python . Itu bagian dari Cpython :-P
mgilson
67

Di balik selimut, i += 1lakukan sesuatu seperti ini:

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

Sementara i = i + 1melakukan sesuatu seperti ini:

i = i.__add__(1)

Ini adalah penyederhanaan yang sedikit berlebihan, tetapi Anda mendapatkan ide: Python memberikan tipe cara untuk menangani +=secara khusus, dengan menciptakan __iadd__metode dan juga __add__.

Tujuannya adalah bahwa tipe yang dapat berubah, seperti list, akan bermutasi di __iadd__(dan kemudian kembali self, kecuali jika Anda melakukan sesuatu yang sangat rumit), sedangkan tipe yang tidak dapat diubah, seperti int, tidak akan mengimplementasikannya.

Sebagai contoh:

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

Karena l2objeknya sama dengan l1, dan Anda bermutasi l1, Anda juga bermutasi l2.

Tapi:

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

Di sini, Anda tidak bermutasi l1; alih-alih, Anda membuat daftar baru l1 + [3],, dan memunculkan kembali nama l1untuk menunjuk padanya, meninggalkan l2menunjuk pada daftar asli.

(Dalam +=versi itu, kamu juga memberontak l1, hanya saja dalam kasus itu kamu listmengikatnya dengan yang sudah terikat, jadi kamu biasanya bisa mengabaikan bagian itu.)

abarnert
sumber
tidak __iadd__benar - benar memanggil __add__jika ada AttributeError?
mgilson
Yah, i.__iadd__jangan menelepon __add__; itu i += 1yang memanggil __add__.
abarnert
errr ... Ya, itu yang saya maksud. Menarik. Saya tidak menyadari bahwa itu dilakukan secara otomatis.
mgilson
3
Upaya pertama sebenarnya i = i.__iadd__(1)- iadd dapat memodifikasi objek di tempat, tetapi tidak harus, dan diharapkan mengembalikan hasilnya dalam kedua kasus.
lvc
Perhatikan bahwa ini berarti operator.iaddpanggilan __add__aktif AttributeError, tetapi tidak dapat mengulang hasilnya ... jadi i=1; operator.iadd(i, 1)kembalikan 2 dan isetel ke 1. Yang agak membingungkan.
abarnert
6

Berikut adalah contoh yang secara langsung dibandingkan i += xdengan 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
sumber