Saya mencoba membuat fungsi di dalam loop:
functions = []
for i in range(3):
def f():
return i
# alternatively: f = lambda: i
functions.append(f)
Masalahnya adalah semua fungsi akhirnya menjadi sama. Alih-alih mengembalikan 0, 1, dan 2, ketiga fungsi tersebut mengembalikan 2:
print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output: [2, 2, 2]
Mengapa ini terjadi, dan apa yang harus saya lakukan untuk mendapatkan 3 fungsi berbeda yang masing-masing menghasilkan 0, 1, dan 2?
Jawaban:
Anda mengalami masalah dengan pengikatan terlambat - setiap fungsi mencari
i
selambat mungkin (dengan demikian, ketika dipanggil setelah akhir loop,i
akan disetel ke2
).Mudah diperbaiki dengan memaksa pengikatan awal: ubah
def f():
menjadidef f(i=i):
seperti ini:Nilai default (kanan-tangan
i
dii=i
adalah nilai default untuk nama argumeni
, yang meninggalkan-tangani
dii=i
) yang memandangdef
waktu, tidak padacall
waktu, jadi pada dasarnya mereka cara untuk secara khusus mencari mengikat awal.Jika Anda khawatir tentang
f
mendapatkan argumen tambahan (dan berpotensi dipanggil secara keliru), ada cara yang lebih canggih yang melibatkan penggunaan closure sebagai "pabrik fungsi":dan dalam penggunaan loop Anda,
f = make_f(i)
bukandef
pernyataan.sumber
Penjelasan
Masalahnya di sini adalah bahwa nilai
i
tidak disimpan saat fungsif
dibuat. Sebaliknya,f
cari nilaii
saat dipanggil .Jika Anda memikirkannya, perilaku ini sangat masuk akal. Faktanya, itulah satu-satunya cara fungsi yang masuk akal dapat bekerja. Bayangkan Anda memiliki fungsi yang mengakses variabel global, seperti ini:
Ketika Anda membaca kode ini, Anda akan - tentu saja - mengharapkannya untuk mencetak "bar", bukan "foo", karena nilai
global_var
telah berubah setelah fungsi dideklarasikan. Hal yang sama terjadi di kode Anda sendiri: Pada saat Anda meneleponf
, nilaii
telah berubah dan disetel ke2
.Solusinya
Sebenarnya ada banyak cara untuk mengatasi masalah ini. Berikut beberapa opsinya:
Paksa pengikatan awal
i
dengan menggunakannya sebagai argumen defaultTidak seperti variabel closure (seperti
i
), argumen default dievaluasi segera saat fungsi ditentukan:Untuk memberikan sedikit wawasan tentang bagaimana / mengapa ini bekerja: Argumen default suatu fungsi disimpan sebagai atribut fungsi; sehingga saat ini nilai
i
yang snapshotted dan disimpan.Gunakan pabrik fungsi untuk menangkap nilai saat ini
i
dalam penutupanAkar masalah Anda
i
adalah variabel yang bisa berubah. Kita dapat mengatasi masalah ini dengan membuat variabel lain yang dijamin tidak akan pernah berubah - dan cara termudah untuk melakukannya adalah dengan menutupnya :Gunakan
functools.partial
untuk mengikat nilai saat inii
kef
functools.partial
memungkinkan Anda melampirkan argumen ke fungsi yang ada. Di satu sisi, itu juga semacam pabrik fungsi.Peringatan: Solusi ini hanya berfungsi jika Anda menetapkan nilai baru ke variabel. Jika Anda memodifikasi objek yang disimpan dalam variabel, Anda akan mengalami masalah yang sama lagi:
Perhatikan bagaimana
i
masih berubah meskipun kami mengubahnya menjadi argumen default! Jika kode Anda bermutasii
, Anda harus mengikat salinan darii
ke fungsi Anda, seperti:def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())
sumber