Membuat fungsi dalam satu lingkaran

102

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?

sharvey
sumber
4
sebagai pengingat untuk diri saya sendiri: docs.python-guide.org/en/latest/writing/gotchas/…
Lu

Jawaban:

167

Anda mengalami masalah dengan pengikatan terlambat - setiap fungsi mencari iselambat mungkin (dengan demikian, ketika dipanggil setelah akhir loop, iakan disetel ke 2).

Mudah diperbaiki dengan memaksa pengikatan awal: ubah def f():menjadi def f(i=i):seperti ini:

def f(i=i):
    return i

Nilai default (kanan-tangan idi i=iadalah nilai default untuk nama argumen i, yang meninggalkan-tangan idi i=i) yang memandang defwaktu, tidak pada callwaktu, jadi pada dasarnya mereka cara untuk secara khusus mencari mengikat awal.

Jika Anda khawatir tentang fmendapatkan argumen tambahan (dan berpotensi dipanggil secara keliru), ada cara yang lebih canggih yang melibatkan penggunaan closure sebagai "pabrik fungsi":

def make_f(i):
    def f():
        return i
    return f

dan dalam penggunaan loop Anda, f = make_f(i)bukan defpernyataan.

Alex Martelli
sumber
7
bagaimana Anda tahu cara memperbaiki hal-hal ini?
alwbtc
3
@alwbtc sebagian besar hanya pengalaman, kebanyakan orang pernah menghadapi hal-hal ini sendiri di beberapa titik.
ruohola
Bisakah Anda menjelaskan mengapa ini berhasil? (Anda menyelamatkan saya pada panggilan balik yang dihasilkan dalam loop, argumen selalu merupakan yang terakhir dari loop jadi terima kasih!)
Vincent Bénet
20

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())
Aran-Fey
sumber