Penjelasan
Masalahnya di sini adalah bahwa nilai i
tidak disimpan saat fungsi f
dibuat. Sebaliknya, f
cari nilai i
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:
global_var = 'foo'
def my_function():
print(global_var)
global_var = 'bar'
my_function()
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 menelepon f
, nilai i
telah berubah dan disetel ke 2
.
Solusinya
Sebenarnya ada banyak cara untuk mengatasi masalah ini. Berikut beberapa opsinya:
Paksa pengikatan awal i
dengan menggunakannya sebagai argumen default
Tidak seperti variabel closure (seperti i
), argumen default dievaluasi segera saat fungsi ditentukan:
for i in range(3):
def f(i=i): # <- right here is the important bit
return i
functions.append(f)
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.
>>> i = 0
>>> def f(i=i):
... pass
>>> f.__defaults__ # this is where the current value of i is stored
(0,)
>>> # assigning a new value to i has no effect on the function's default arguments
>>> i = 5
>>> f.__defaults__
(0,)
Gunakan pabrik fungsi untuk menangkap nilai saat ini i
dalam penutupan
Akar 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 :
def f_factory(i):
def f():
return i # i is now a *local* variable of f_factory and can't ever change
return f
for i in range(3):
f = f_factory(i)
functions.append(f)
Gunakan functools.partial
untuk mengikat nilai saat ini i
kef
functools.partial
memungkinkan Anda melampirkan argumen ke fungsi yang ada. Di satu sisi, itu juga semacam pabrik fungsi.
import functools
def f(i):
return i
for i in range(3):
f_with_i = functools.partial(f, i) # important: use a different variable than "f"
functions.append(f_with_i)
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:
>>> i = [] # instead of an int, i is now a *mutable* object
>>> def f(i=i):
... print('i =', i)
...
>>> i.append(5) # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]
Perhatikan bagaimana i
masih berubah meskipun kami mengubahnya menjadi argumen default! Jika kode Anda bermutasi i
, Anda harus mengikat salinan dari i
ke fungsi Anda, seperti:
def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())