Penjelasan
Masalahnya di sini adalah bahwa nilai itidak disimpan saat fungsi fdibuat. Sebaliknya, fcari nilai isaat 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_vartelah berubah setelah fungsi dideklarasikan. Hal yang sama terjadi di kode Anda sendiri: Pada saat Anda menelepon f, nilai itelah berubah dan disetel ke 2.
Solusinya
Sebenarnya ada banyak cara untuk mengatasi masalah ini. Berikut beberapa opsinya:
Paksa pengikatan awal idengan 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 iyang 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 idalam penutupan
Akar masalah Anda iadalah 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.partialuntuk mengikat nilai saat ini ikef
functools.partialmemungkinkan 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 imasih berubah meskipun kami mengubahnya menjadi argumen default! Jika kode Anda bermutasi i , Anda harus mengikat salinan dari ike fungsi Anda, seperti:
def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())