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?
Odpowiedzi:
Występuje problem z późnym wiązaniem - każda funkcja wyszukuje dane
i
tak późno, jak to możliwe (w związku z tym po wywołaniu po zakończeniu pętlii
zostanie ustawiona na2
).Łatwo naprawione przez wymuszenie wczesnego wiązania: zmień
def f():
nadef f(i=i):
następującą:def f(i=i): return i
Domyślne wartości (prawa ręka-
i
wi=i
jest wartością domyślną nazwą zmienneji
, która jest po lewej stroniei
wi=i
) są spojrzał nadef
czasie, nie nacall
czasie, więc w zasadzie są one drogę do specjalnie szuka wcześnie wiążące.Jeśli martwisz
f
się 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)
zamiastdef
instrukcji.źródło
Wyjaśnienie
Problem polega na tym, że wartość
i
nie jest zapisywana podczas tworzenia funkcjif
. Raczejf
wyszukuje wartość,i
kiedy 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_var
zmieniła się po zadeklarowaniu funkcji. To samo dzieje się w Twoim własnym kodzie: w momencie wywołaniaf
wartość zmienneji
zmienił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ślnegoW 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ść
i
jest 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ść
i
zamknięciaPrzyczyną twojego problemu jest to, że
i
zmienna 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śći
dof
functools.partial
umoż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
i
to się zmieniło, mimo że zmieniliśmy go w argument domyślny! Jeśli twój kod ulega mutacjii
, musisz powiązać kopię zei
swoją funkcją, na przykład:def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())
źródło