Baru-baru ini saya mulai bermain-main dengan Python dan saya menemukan sesuatu yang aneh dalam cara penutupan. Pertimbangkan kode berikut:
adders=[0,1,2,3]
for i in [0,1,2,3]:
adders[i]=lambda a: i+a
print adders[1](3)
Itu membangun array sederhana fungsi yang mengambil input tunggal dan mengembalikan input yang ditambahkan oleh angka. Fungsi-fungsi dibangun dalam for
loop di mana iterator i
berjalan dari 0
ke 3
. Untuk masing-masing angka-angka ini lambda
fungsi dibuat yang menangkap i
dan menambahkannya ke input fungsi. Baris terakhir memanggil lambda
fungsi kedua dengan 3
sebagai parameter. Yang mengejutkan saya, hasilnya adalah 6
.
Saya mengharapkan sebuah 4
. Alasan saya adalah: dalam Python semuanya adalah objek dan karenanya setiap variabel adalah penunjuk yang penting. Saat membuat lambda
closure for i
, saya berharap untuk menyimpan pointer ke objek integer yang saat ini ditunjuk oleh i
. Itu berarti bahwa ketika i
menetapkan objek integer baru itu seharusnya tidak mempengaruhi penutupan yang dibuat sebelumnya. Sayangnya, memeriksa adders
array di dalam debugger menunjukkan hal itu. Semua lambda
fungsi mengacu pada nilai terakhir i
, 3
yang mengakibatkan adders[1](3)
kembali 6
.
Yang membuat saya bertanya-tanya tentang hal berikut:
- Apa tepatnya yang ditangkap penutup?
- Apa cara paling elegan untuk meyakinkan
lambda
fungsi untuk menangkap nilai saat inii
dengan cara yang tidak akan terpengaruh ketikai
mengubah nilainya?
i
meninggalkan namespace?print i
tidak akan berhasil setelah loop. Tapi saya mengujinya sendiri dan sekarang saya mengerti maksud Anda - itu berhasil. Saya tidak tahu bahwa variabel loop tetap setelah loop body dengan python.if
,with
,try
dllJawaban:
Pertanyaan kedua Anda telah dijawab, tetapi untuk pertanyaan pertama Anda:
Pelingkupan dalam Python bersifat
dinamis danleksikal. Penutupan akan selalu mengingat nama dan ruang lingkup variabel, bukan objek yang ditunjuknya. Karena semua fungsi dalam contoh Anda dibuat dalam cakupan yang sama dan menggunakan nama variabel yang sama, mereka selalu merujuk ke variabel yang sama.EDIT: Mengenai pertanyaan Anda yang lain tentang cara mengatasi hal ini, ada dua cara yang muncul di pikiran:
Cara yang paling ringkas, tetapi tidak sepenuhnya setara adalah yang direkomendasikan oleh Adrien Plisson . Buat lambda dengan argumen tambahan, dan setel nilai default argumen tambahan ke objek yang ingin Anda pertahankan.
Sedikit lebih bertele-tele tetapi lebih sedikit peretasan akan menciptakan ruang lingkup baru setiap kali Anda membuat lambda:
Ruang lingkup di sini dibuat menggunakan fungsi baru (lambda, untuk singkatnya), yang mengikat argumennya, dan meneruskan nilai yang ingin Anda ikat sebagai argumen. Namun, dalam kode nyata, Anda kemungkinan besar akan memiliki fungsi biasa alih-alih lambda untuk membuat lingkup baru:
sumber
set!
. lihat di sini untuk apa sebenarnya ruang lingkup dinamis: voidspace.org.uk/python/articles/code_blocks.shtml .Anda dapat memaksa penangkapan variabel menggunakan argumen dengan nilai default:
idenya adalah mendeklarasikan parameter (dinamai secara cerdik
i
) dan memberikannya nilai default dari variabel yang ingin Anda tangkap (nilaii
)sumber
Untuk melengkapi jawaban lain untuk pertanyaan kedua Anda: Anda dapat menggunakan sebagian dalam modul functools .
Dengan mengimpor add dari operator ketika Chris Lutz mengusulkan contohnya menjadi:
sumber
Pertimbangkan kode berikut:
Saya pikir kebanyakan orang tidak akan menganggap ini membingungkan sama sekali. Itu adalah perilaku yang diharapkan.
Jadi, mengapa orang berpikir itu akan berbeda ketika dilakukan dalam satu lingkaran? Saya tahu saya melakukan kesalahan itu sendiri, tetapi saya tidak tahu mengapa. Itu adalah loop? Atau mungkin lambda?
Lagipula, loop hanyalah versi pendek dari:
sumber
i
variabel yang sama sedang diakses untuk setiap fungsi lambda.Untuk menjawab pertanyaan kedua Anda, cara paling elegan untuk melakukan ini adalah dengan menggunakan fungsi yang mengambil dua parameter, bukan array:
Namun, menggunakan lambda di sini agak konyol. Python memberi kita
operator
modul, yang menyediakan antarmuka fungsional untuk operator dasar. Lambda di atas memiliki overhead yang tidak perlu hanya untuk memanggil operator tambahan:Saya mengerti bahwa Anda bermain-main, mencoba menjelajahi bahasa, tapi saya tidak bisa membayangkan situasi saya akan menggunakan berbagai fungsi di mana keanehan pelingkupan Python akan menghalangi.
Jika Anda mau, Anda bisa menulis kelas kecil yang menggunakan sintaks pengindeksan array:
sumber
Berikut adalah contoh baru yang menyoroti struktur data dan konten penutupan, untuk membantu mengklarifikasi ketika konteks terlampir "disimpan."
Apa yang ada dalam penutupan?
Khususnya, my_str tidak ada dalam penutupan f1.
Apa yang ada di penutupan f2?
Perhatikan (dari alamat memori) bahwa kedua penutupan mengandung objek yang sama. Jadi, Anda dapat mulai memikirkan fungsi lambda sebagai referensi untuk ruang lingkup. Namun, my_str tidak dalam penutupan untuk f_1 atau f_2, dan saya tidak dalam penutupan untuk f_3 (tidak ditampilkan), yang menunjukkan objek penutupan itu sendiri adalah objek yang berbeda.
Apakah objek penutupan itu sendiri adalah objek yang sama?
sumber
int object at [address X]>
membuat saya berpikir penutupan menyimpan [alamat X] AKA referensi. Namun, [alamat X] akan berubah jika variabel dipindahkan setelah pernyataan lambda.