Saya baru menyadari bahwa dengan Python, jika ada yang menulis
for i in a:
i += 1
Elemen-elemen dari daftar asli a
sebenarnya tidak akan terpengaruh sama sekali, karena variabel i
ternyata hanya salinan dari elemen asli di a
.
Untuk memodifikasi elemen asli,
for index, i in enumerate(a):
a[index] += 1
akan dibutuhkan.
Saya sangat terkejut dengan perilaku ini. Ini tampaknya sangat berlawanan dengan intuisi, tampaknya berbeda dari bahasa lain dan telah mengakibatkan kesalahan pada kode saya yang harus saya debug sejak lama hari ini.
Saya telah membaca Tutorial Python sebelumnya. Hanya untuk memastikan, saya memeriksa buku itu lagi sekarang, dan bahkan tidak menyebutkan perilaku ini sama sekali.
Apa alasan di balik desain ini? Apakah ini diharapkan menjadi praktik standar dalam banyak bahasa sehingga tutorial percaya bahwa pembaca harus mendapatkannya secara alami? Dalam bahasa lain apa perilaku yang sama pada iterasi hadir, yang harus saya perhatikan di masa depan?
i
tidak berubah atau Anda sedang melakukan operasi yang tidak bermutasi. Dengan daftar bersarangfor i in a: a.append(1)
akan memiliki perilaku yang berbeda; Python tidak menyalin daftar bersarang. Namun integer tidak dapat diubah dan penambahan mengembalikan objek baru, itu tidak mengubah yang lama.a=[1,2,3];a.forEach(i => i+=1);alert(a)
. Sama di C #i = i + 1
untuk mempengaruhia
?Jawaban:
Saya sudah menjawab pertanyaan yang sama belakangan ini dan sangat penting untuk disadari yang
+=
dapat memiliki arti yang berbeda:Jika tipe data mengimplementasikan penambahan di tempat (yaitu memiliki
__iadd__
fungsi yang berfungsi dengan benar ) maka data yangi
mengacu diperbarui (tidak masalah apakah itu ada dalam daftar atau di tempat lain).Jika tipe data tidak menerapkan
__iadd__
metodei += x
pernyataan hanya untuk gula sintaksisi = i + x
, sehingga nilai baru dibuat dan ditugaskan ke nama variabeli
.Jika tipe data mengimplementasikan
__iadd__
tetapi melakukan sesuatu yang aneh. Bisa jadi itu diperbarui ... atau tidak - itu tergantung pada apa yang diterapkan di sana.Bilangan bulat ular, float, string tidak diimplementasikan
__iadd__
sehingga ini tidak akan diperbarui di tempat. Namun tipe data lain sukanumpy.array
ataulist
menerapkannya dan akan berperilaku seperti yang Anda harapkan. Jadi ini bukan masalah salin atau tidak-salin ketika iterasi (biasanya tidak melakukan salin untuklist
s dantuple
s - tetapi itu juga tergantung pada implementasi wadah__iter__
dan__getitem__
metode!) - ini lebih merupakan masalah tipe data Anda telah menyimpannya dia
.sumber
Klarifikasi - terminologi
Python tidak membedakan antara konsep referensi dan pointer . Mereka biasanya hanya menggunakan referensi istilah , tetapi jika Anda membandingkan dengan bahasa seperti C ++ yang memang memiliki perbedaan itu - itu jauh lebih dekat dengan pointer .
Karena penanya jelas berasal dari latar belakang C ++, dan karena perbedaan itu - yang diperlukan untuk penjelasan - tidak ada dalam Python, saya telah memilih untuk menggunakan terminologi C ++, yaitu:
void foo(int x);
adalah tanda tangan dari suatu fungsi yang menerima bilangan bulat berdasarkan nilai .void foo(int* x);
adalah tanda tangan dari fungsi yang menerima integer oleh pointer .void foo(int& x);
adalah tanda tangan dari fungsi yang menerima bilangan bulat dengan referensi .Apa maksud Anda "berbeda dari bahasa lain"? Sebagian besar bahasa yang saya tahu dukungan untuk setiap loop menyalin elemen kecuali jika diperintahkan sebaliknya.
Khusus untuk Python (meskipun banyak dari alasan ini dapat berlaku untuk bahasa lain dengan konsep arsitektur atau filosofi yang serupa):
Perilaku ini dapat menyebabkan bug bagi orang yang tidak menyadarinya, tetapi perilaku alternatif dapat menyebabkan bug bahkan bagi mereka yang menyadarinya . Saat Anda menetapkan variabel (
i
) Anda biasanya tidak berhenti dan mempertimbangkan semua variabel lain yang akan diubah karena itu (a
). Membatasi ruang lingkup yang sedang Anda kerjakan adalah faktor utama dalam mencegah kode spageti, dan oleh karena itu iterasi dengan salinan biasanya merupakan standar bahkan dalam bahasa yang mendukung iterasi dengan referensi.Variabel python selalu berupa pointer tunggal, jadi lebih murah untuk beralih dengan salinan - lebih murah daripada mengulang dengan referensi, yang akan membutuhkan penangguhan tambahan setiap kali Anda mengakses nilai.
Python tidak memiliki konsep variabel referensi seperti - misalnya - C ++. Yaitu, semua variabel dalam Python sebenarnya adalah referensi, tetapi dalam arti bahwa mereka adalah pointer - bukan referensi konstanta di belakang layar seperti
type& name
argumen C ++ . Karena konsep ini tidak ada di Python, mengimplementasikan iterasi dengan referensi - apalagi menjadikannya default! - akan membutuhkan menambahkan lebih banyak kompleksitas ke bytecode.for
Pernyataan Python tidak hanya bekerja pada array, tetapi pada konsep generator yang lebih umum. Di belakang layar, Python memanggiliter
array Anda untuk mendapatkan objek yang - ketika Anda memanggilnyanext
- baik mengembalikan elemen berikutnya atauraise
saStopIteration
. Ada beberapa cara untuk mengimplementasikan generator di Python, dan akan jauh lebih sulit untuk mengimplementasikannya untuk iterasi-oleh-referensi.sumber
*it = ...
- tetapi sintaks semacam ini sudah menunjukkan Anda memodifikasi sesuatu di tempat lain - yang membuat alasan # 1 kurang dari masalah. Alasan # 2 dan # 3 tidak berlaku juga, karena dalam C ++ penyalinan mahal dan konsep variabel referensi ada. Adapun alasan # 4 - kemampuan untuk mengembalikan referensi memungkinkan implementasi sederhana untuk semua kasus.Tidak ada jawaban di sini yang memberi Anda kode untuk dikerjakan untuk benar-benar menggambarkan mengapa ini terjadi di tanah Python. Dan ini menyenangkan untuk dilihat dalam pendekatan yang lebih mendalam jadi begini.
Alasan utama bahwa ini tidak berfungsi seperti yang Anda harapkan adalah karena dalam Python, ketika Anda menulis:
itu tidak melakukan apa yang Anda pikir sedang dilakukan. Bilangan bulat tidak berubah. Ini bisa dilihat ketika Anda melihat apa objek sebenarnya dalam Python:
Fungsi id mewakili nilai unik dan konstan untuk objek dalam masa pakai itu. Secara konseptual, ini dipetakan secara longgar ke alamat memori dalam C / C ++. Menjalankan kode di atas:
Ini berarti yang pertama
a
tidak lagi sama dengan yang keduaa
, karena id mereka berbeda. Secara efektif mereka berada di lokasi yang berbeda dalam memori.Namun, dengan suatu benda, berbagai hal bekerja secara berbeda. Saya telah menimpa
+=
operator di sini:Menjalankan ini menghasilkan output berikut:
Perhatikan bahwa atribut id dalam kasus ini sebenarnya sama untuk kedua iterasi, meskipun nilai objek berbeda (Anda juga bisa menemukan
id
nilai int yang dipegang objek, yang akan berubah saat bermutasi - karena bilangan bulat tidak berubah).Bandingkan dengan ketika Anda menjalankan latihan yang sama dengan objek yang tidak berubah:
Output ini:
Beberapa hal yang perlu diperhatikan di sini. Pertama, di loop dengan
+=
, Anda tidak lagi menambahkan ke objek asli. Dalam hal ini, karena int adalah salah satu tipe yang tidak dapat diubah dalam Python , python menggunakan id yang berbeda. Juga menarik untuk dicatat bahwa Python menggunakan dasar yang samaid
untuk beberapa variabel dengan nilai tetap yang sama:tl; dr - Python memiliki beberapa tipe yang tidak dapat diubah, yang menyebabkan perilaku yang Anda lihat. Untuk semua jenis yang bisa berubah, harapan Anda benar.
sumber
@ Idan jawaban melakukan pekerjaan yang baik untuk menjelaskan mengapa Python tidak memperlakukan variabel loop sebagai pointer seperti yang Anda mungkin dalam C, tetapi perlu dijelaskan secara lebih mendalam bagaimana potongan kode membongkar, seperti dalam Python banyak bit yang tampak sederhana kode sebenarnya akan menjadi panggilan untuk metode built in . Untuk mengambil contoh pertama Anda
Ada dua hal yang harus dibongkar:
for _ in _:
sintaks dan_ += _
sintaks. Untuk mengambil loop pertama, seperti bahasa lain, Python memilikifor-each
loop yang pada dasarnya adalah sintaksis gula untuk pola iterator. Dalam Python, sebuah iterator adalah objek yang mendefinisikan.__next__(self)
metode yang mengembalikan elemen saat ini dalam urutan, maju ke yang berikutnya dan akan menaikkanStopIteration
ketika tidak ada lagi item dalam urutan. Sebuah Iterable adalah obyek yang mendefinisikan sebuah.__iter__(self)
metode yang kembali iterator.(NB: an
Iterator
juga merupakanIterable
dan mengembalikan dirinya dari.__iter__(self)
metodenya.)Python biasanya akan memiliki fungsi bawaan yang mendelegasikan ke metode menggarisbawahi kustom ganda. Jadi ada
iter(o)
yang memutuskan untuko.__iter__()
dannext(o)
yang memutuskan untuko.__next__()
. Perhatikan bahwa fungsi bawaan ini sering akan mencoba definisi standar yang masuk akal jika metode yang akan didelegasikan tidak didefinisikan. Misalnya,len(o)
biasanya memutuskan untuko.__len__()
tetapi jika metode itu tidak didefinisikan maka akan mencobaiter(o).__len__()
.A for loop pada dasarnya didefinisikan dalam hal
next()
,iter()
dan struktur kontrol yang lebih mendasar. Secara umum kodeakan dibongkar ke sesuatu seperti
Jadi dalam hal ini
akan dibongkar
Setengah lainnya adalah
i += 1
. Secara umum%ASSIGN% += %EXPR%
akan dibongkar%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
. Di sini__iadd__(self, other)
melakukan penambahan inplace dan mengembalikan sendiri.(NB Ini adalah kasus lain di mana Python akan memilih alternatif jika metode utama tidak didefinisikan. Jika objek tidak mengimplementasikannya
__iadd__
akan jatuh kembali__add__
. Ini benar-benar melakukan ini dalam kasus ini karenaint
tidak mengimplementasikan__iadd__
- yang masuk akal karena mereka tidak dapat diubah dan karenanya tidak dapat dimodifikasi di tempat.)Jadi kode Anda di sini terlihat seperti
di mana kita dapat mendefinisikan
Ada sedikit lebih banyak hal yang terjadi dalam bit kode kedua Anda. Dua hal baru yang perlu kita ketahui adalah bahwa
%ARG%[%KEY%] = %VALUE%
akan dibongkar(%ARG%).__setitem__(%KEY%, %VALUE%)
dan%ARG%[%KEY%]
dibongkar(%ARG%).__getitem__(%KEY%)
. Dengan menyatukan pengetahuan ini, kami dapata[ix] += 1
membongkara.__setitem__(ix, a.__getitem__(ix).__add__(1))
(lagi:__add__
daripada__iadd__
karena__iadd__
tidak diterapkan oleh ints). Kode akhir kami terlihat seperti:Untuk benar-benar menjawab pertanyaan Anda mengapa yang pertama tidak mengubah daftar sedangkan yang kedua tidak, dalam potongan pertama kami kita mendapatkan
i
darinext(_a_iter)
, yang berartii
akan menjadiint
. Karenaint
tidak dapat diubah pada tempatnya,i += 1
tidak melakukan apa pun pada daftar. Dalam kasus kedua kami, kami sekali lagi tidak mengubahint
tetapi memodifikasi daftar dengan memanggil__setitem__
.Alasan untuk seluruh latihan rumit ini adalah karena saya pikir ini mengajarkan pelajaran berikut tentang Python:
Metode garis bawah ganda adalah rintangan ketika memulai, tetapi mereka sangat penting untuk mendukung reputasi "runnable pseudocode" Python. Seorang programmer Python yang baik akan memiliki pemahaman menyeluruh tentang metode ini dan bagaimana mereka dipanggil dan akan mendefinisikan mereka di mana pun masuk akal untuk melakukannya.
Sunting : @deltab mengoreksi penggunaan istilah "koleksi" saya yang ceroboh.
sumber
__len__
dan__contains__
+=
bekerja secara berbeda berdasarkan pada apakah nilai saat ini bisa berubah atau tidak berubah . Ini adalah alasan utama yang terlihat lama untuk diimplementasikan dalam Python, karena pengembang Python takut itu akan membingungkan.Jika
i
int, maka itu tidak dapat diubah karena int tidak dapat diubah, dan dengan demikian jika nilaii
perubahan maka harus selalu menunjuk ke objek lain:Namun jika sisi kiri bisa berubah , maka + = sebenarnya bisa mengubahnya; seperti jika itu daftar:
Di loop for Anda,
i
merujuk ke setiap elemena
pada gilirannya. Jika itu adalah bilangan bulat, maka kasus pertama berlaku, dan hasilnyai += 1
harus mengacu pada objek bilangan bulat lainnya. Daftara
tentu saja masih memiliki elemen yang sama seperti sebelumnya.sumber
i = 1
diaturi
ke objek integer yang tidak dapat diubah, makai = []
harus ditetapkani
ke objek daftar yang tidak dapat diubah. Dengan kata lain, mengapa objek integer tidak dapat diubah dan daftar objek bisa berubah? Saya tidak melihat logika di balik ini.list
mengimplementasikan metode yang mengubah isinya,int
tidak.[]
adalah objek daftar yang dapat diubah, dani = []
marii
merujuk ke objek itu.+=
operator / metode untuk berperilaku serupa (prinsip kejutan paling sedikit) untuk kedua jenis: mengubah objek asli atau mengembalikan salinan yang dimodifikasi untuk bilangan bulat dan daftar.+=
mengejutkan dengan Python, tetapi dirasakan bahwa opsi lain yang Anda sebutkan juga akan mengejutkan, atau setidaknya kurang praktis (mengubah objek asli tidak dapat dilakukan dengan jenis nilai yang paling umum Anda menggunakan + = with, ints. Dan menyalin seluruh daftar jauh lebih mahal daripada memutasinya, Python tidak menyalin hal-hal seperti daftar dan kamus kecuali disuruh secara eksplisit). Itu adalah perdebatan besar saat itu.Loop di sini agak tidak relevan. Sama seperti parameter fungsi atau argumen, mengatur loop untuk seperti itu pada dasarnya hanya tugas yang tampak mewah.
Bilangan bulat tidak berubah. Satu-satunya cara untuk memodifikasinya adalah dengan membuat integer baru, dan menugaskannya dengan nama yang sama dengan aslinya.
Semantik Python untuk penugasan peta langsung ke C (diberikan pointer PyObject * CPython yang diberikan), dengan satu-satunya peringatan adalah bahwa semuanya adalah pointer, dan Anda tidak diizinkan memiliki pointer ganda. Pertimbangkan kode berikut:
Apa yang terjadi? Mencetak
1
. Mengapa? Ini sebenarnya kira-kira setara dengan kode C berikut:Dalam kode C, jelas bahwa nilai
a
sepenuhnya tidak terpengaruh.Adapun mengapa daftar tampaknya berfungsi, jawabannya pada dasarnya hanya bahwa Anda menugaskan untuk nama yang sama. Daftar bisa berubah. Identitas objek yang dinamai
a[0]
akan berubah, tetapia[0]
masih merupakan nama yang valid. Anda dapat memeriksa ini dengan kode berikut:Tapi, ini tidak spesial untuk daftar. Ganti
a[0]
kode itu dengany
dan Anda mendapatkan hasil yang sama persis.sumber