Tworzenie funkcji w pętli

104

Próbuję stworzyć funkcje wewnątrz pętli:

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

Problem polega na tym, że wszystkie funkcje są takie same. Zamiast zwracać 0, 1 i 2, wszystkie trzy funkcje zwracają 2:

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

Dlaczego tak się dzieje i co powinienem zrobić, aby uzyskać 3 różne funkcje, które wyświetlają odpowiednio 0, 1 i 2?

sharvey
źródło
4
jako przypomnienie dla mnie: docs.python-guide.org/en/latest/writing/gotchas/…
Chuntao Lu

Odpowiedzi:

168

Występuje problem z późnym wiązaniem - każda funkcja wyszukuje dane itak późno, jak to możliwe (w związku z tym po wywołaniu po zakończeniu pętli izostanie ustawiona na 2).

Łatwo naprawione przez wymuszenie wczesnego wiązania: zmień def f():na def f(i=i):następującą:

def f(i=i):
    return i

Domyślne wartości (prawa ręka- iw i=ijest wartością domyślną nazwą zmiennej i, która jest po lewej stronie iw i=i) są spojrzał na defczasie, nie na callczasie, więc w zasadzie są one drogę do specjalnie szuka wcześnie wiążące.

Jeśli martwisz fsię dodatkowym argumentem (a tym samym potencjalnie błędnym wywołaniem), istnieje bardziej wyrafinowany sposób, który obejmuje użycie zamknięcia jako „fabryki funkcji”:

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

aw pętli użyj f = make_f(i)zamiast definstrukcji.

Alex Martelli
źródło
7
skąd wiesz, jak to naprawić?
alwbtc
3
@alwbtc to głównie doświadczenie, większość ludzi musiała w pewnym momencie stawić czoła tym problemom samodzielnie.
ruohola
Czy możesz wyjaśnić, dlaczego to działa? (Oszczędzacie mnie na wywołaniu zwrotnym generowanym w pętli, argumenty były zawsze
ostatnimi
22

Wyjaśnienie

Problem polega na tym, że wartość inie jest zapisywana podczas tworzenia funkcji f. Raczej fwyszukuje wartość, ikiedy jest wywoływana .

Jeśli się nad tym zastanowić, to zachowanie ma sens. W rzeczywistości jest to jedyny rozsądny sposób działania funkcji. Wyobraź sobie, że masz funkcję, która uzyskuje dostęp do zmiennej globalnej, na przykład:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Czytając ten kod, spodziewalibyśmy się - oczywiście - wypisać "bar", a nie "foo", ponieważ wartość global_varzmieniła się po zadeklarowaniu funkcji. To samo dzieje się w Twoim własnym kodzie: w momencie wywołania fwartość zmiennej izmieniła się i została ustawiona na2 .

Rozwiązanie

W rzeczywistości istnieje wiele sposobów rozwiązania tego problemu. Oto kilka opcji:

  • Wymuś wczesne wiązanie i, używając go jako argumentu domyślnego

    W przeciwieństwie do zmiennych zamykających (takich jak i), argumenty domyślne są obliczane natychmiast po zdefiniowaniu funkcji:

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)
    

    Aby dać trochę wglądu w to, jak / dlaczego to działa: Domyślne argumenty funkcji są przechowywane jako atrybut funkcji; Więc bieżąca wartość ijest zapisywana i zapisywana.

    >>> 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,)
    
  • Użyj fabryki funkcji, aby uchwycić bieżącą wartość izamknięcia

    Przyczyną twojego problemu jest to, że izmienna może się zmieniać. Możemy obejść ten problem, tworząc kolejną zmienną, która na pewno nigdy się nie zmieni - a najłatwiejszym sposobem na to jest zamknięcie :

    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)
    
  • Posługiwać się functools.partial związać bieżącą wartość idof

    functools.partialumożliwia dołączenie argumentów do istniejącej funkcji. W pewnym sensie jest to też rodzaj fabryki funkcji.

    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)
    

Uwaga: te rozwiązania działają tylko wtedy, gdy przypiszesz nową wartość do zmiennej. Jeśli ty zmodyfikujesz obiekt przechowywany w zmiennej, ponownie wystąpi ten sam problem:

>>> 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]

Zwróć uwagę, jak ito się zmieniło, mimo że zmieniliśmy go w argument domyślny! Jeśli twój kod ulega mutacji i , musisz powiązać kopię ze iswoją funkcją, na przykład:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
Aran-Fey
źródło