Ostatnio zacząłem grać z Pythonem i natrafiłem na coś dziwnego w sposobie działania zamknięć. Rozważ następujący kod:
adders=[0,1,2,3]
for i in [0,1,2,3]:
adders[i]=lambda a: i+a
print adders[1](3)
Buduje prostą tablicę funkcji, które pobierają pojedyncze dane wejściowe i zwracają dane dodane przez liczbę. Funkcje są zbudowane w for
pętli, w której iterator i
biegnie od 0
do 3
. Dla każdej z tych liczb lambda
tworzona jest funkcja, która przechwytuje i
i dodaje ją do danych wejściowych funkcji. Ostatnia linia wywołuje drugą lambda
funkcję z 3
parametrem. Ku mojemu zaskoczeniu wynik był 6
.
Spodziewałem się 4
. Moje rozumowanie brzmiało: w Pythonie wszystko jest przedmiotem, a zatem każda zmienna jest niezbędnym wskaźnikiem do niej. Podczas tworzenia lambda
zamknięć i
, spodziewałem się, że zapisze wskaźnik do obiektu liczb całkowitych, na który wskazuje aktualnie i
. Oznacza to, że i
przypisany nowy obiekt liczby całkowitej nie powinien wpływać na wcześniej utworzone zamknięcia. Niestety, sprawdzenie adders
tablicy w debuggerze pokazuje, że tak. Wszystkie lambda
funkcje znajdują się w ostatniej wartości i
, 3
, co powoduje adders[1](3)
powrót 6
.
Zastanawiam się co do następujących rzeczy:
- Co dokładnie rejestrują zamknięcia?
- Jaki jest najbardziej elegancki sposób przekonania
lambda
funkcji do przechwytywania bieżącej wartościi
w sposób, który nie ulegnie zmianie poi
zmianie jej wartości?
i
opuszcza przestrzeń nazw?print i
że nie będzie działać po pętli. Ale przetestowałem to dla siebie i teraz rozumiem, co masz na myśli - to działa. Nie miałem pojęcia, że zmienne pętli pozostały po pytle w ciele pętli.if
,with
,try
itd.Odpowiedzi:
Odpowiedzi na twoje drugie pytanie, ale jak na twoje pierwsze:
Określanie zakresu w Pythonie jest
dynamiczne ileksykalne. Zamknięcie zawsze zapamięta nazwę i zakres zmiennej, a nie obiekt, na który wskazuje. Ponieważ wszystkie funkcje w twoim przykładzie są utworzone w tym samym zakresie i używają tej samej nazwy zmiennej, zawsze odnoszą się do tej samej zmiennej.EDYCJA: Jeśli chodzi o inne pytanie, jak przezwyciężyć ten problem, przychodzą mi na myśl dwa sposoby:
Najbardziej zwięzły, ale nie do końca równoważny sposób jest zalecany przez Adrien Plisson . Utwórz lambda z dodatkowym argumentem i ustaw domyślną wartość dodatkowego argumentu na obiekt, który chcesz zachować.
Nieco bardziej szczegółowe, ale mniej zhackowane byłoby utworzenie nowego zakresu za każdym razem, gdy tworzysz lambda:
Zakres tutaj jest tworzony przy użyciu nowej funkcji (zwięzłej lambda), która wiąże swój argument i przekazuje wartość, którą chcesz powiązać jako argument. Jednak w prawdziwym kodzie najprawdopodobniej będziesz mieć zwykłą funkcję zamiast lambda, aby utworzyć nowy zakres:
źródło
set!
. zobacz tutaj, czym tak naprawdę jest zakres dynamiczny: voidspace.org.uk/python/articles/code_blocks.shtml .możesz wymusić przechwycenie zmiennej za pomocą argumentu o wartości domyślnej:
chodzi o to, aby zadeklarować parametr (sprytnie nazwane
i
) i nadać mu wartość domyślną zmiennej chcesz uchwycić (wartośći
)źródło
Dla kompletności kolejna odpowiedź na twoje drugie pytanie: Możesz użyć częściowego w module funkools .
W przypadku importowania add od operatora, jak zaproponował Chris Lutz, przykładem staje się:
źródło
Rozważ następujący kod:
Myślę, że większość ludzi nie uzna tego za mylące. To jest oczekiwane zachowanie.
Dlaczego więc ludzie myślą, że byłoby inaczej, gdy odbywa się to w pętli? Wiem, że sam popełniłem ten błąd, ale nie wiem dlaczego. To jest pętla? A może lambda?
W końcu pętla jest po prostu krótszą wersją:
źródło
i
zmienna jest dostępna dla każdej funkcji lambda.W odpowiedzi na twoje drugie pytanie najbardziej eleganckim sposobem na zrobienie tego byłoby użycie funkcji, która pobiera dwa parametry zamiast tablicy:
Jednak użycie lambda tutaj jest trochę głupie. Python daje nam
operator
moduł, który zapewnia funkcjonalny interfejs dla podstawowych operatorów. Powyższa lambda ma niepotrzebny narzut, aby wywołać operatora dodawania:Rozumiem, że bawisz się, próbujesz poznawać język, ale nie wyobrażam sobie sytuacji, w której użyłbym szeregu funkcji, w których przeszkadzałaby dziwna zakresowość Pythona.
Jeśli chcesz, możesz napisać małą klasę, która używa składni indeksowania tablic:
źródło
Oto nowy przykład, który podkreśla strukturę danych i treść zamknięcia, aby pomóc wyjaśnić, kiedy kontekst otaczający jest „zapisywany”.
Co jest w zamknięciu?
W szczególności, my_str nie jest w zamknięciu F1.
Co zawiera f2?
Zauważ (z adresów pamięci), że oba zamknięcia zawierają te same obiekty. Możesz więc zacząć myśleć o funkcji lambda jako o odwołaniu do zakresu. Jednak my_str nie znajduje się w zamknięciu dla f_1 lub f_2, a i nie znajduje się w zamknięciu dla f_3 (nie pokazano), co sugeruje, że same obiekty zamknięcia są odrębnymi obiektami.
Czy same obiekty zamknięcia są tym samym przedmiotem?
źródło
int object at [address X]>
sprawiły, że pomyślałem, że zamknięcie przechowuje [adres X] AKA odniesienie. Jednak [adres X] zmieni się, jeśli zmienna zostanie ponownie przypisana po instrukcji lambda.