programowanieProgramista Python

Jak działają zamknięcia (closures) w Pythonie? Jakie są szczegóły dostępu do zmiennych funkcji zewnętrznej i jak unikać pułapek związanych z mutowalnością zmiennych?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Zamknięcia (closures) — potężny mechanizm Pythona, pozwalający na tworzenie funkcji z "zapamiętanymi" danymi z otaczającego kontekstu.

Historia pytania

W językach funkcyjnych oraz w Pythonie funkcje są obiektami pierwszorzędnymi. Funkcja może zwrócić nową funkcję, która 'zamknie' zmienne ze swojej zewnętrznej przestrzeni widzenia.

Problem

Programiści często mylą się, jak dokładnie zamknięte zmienne "żyją" wewnątrz zamknięcia, gdy są odczytywane lub zapisywane, oraz jak bezpiecznie z nimi pracować (szczególnie z obiektami mutowalnymi).

Rozwiązanie

Jeżeli wewnętrzna funkcja korzysta z zmiennych zewnętrznej funkcji, są one automatycznie "zapamiętywane" — nawet jeśli zewnętrzna funkcja już zakończyła działanie. Dla odczytu zmiennej wszystko jest oczywiste, ale jeśli musisz zmienić wartość — należy użyć słowa kluczowego nonlocal. Przy pracy z obiektami mutowalnymi (listy, słowniki) to szczególna strefa ryzyka.

Przykład:

def outer(): count = 0 def inner(): nonlocal count count += 1 return count return inner counter = outer() print(counter()) # 1 print(counter()) # 2

Kluczowe cechy:

  • Funkcja zagnieżdżona zapamiętuje wartości zmiennych, które były w obszarze widzenia w momencie jej utworzenia.
  • Aby zmienić zamknięte zmienne, użyj nonlocal (inaczej operacja tworzy lokalną zmienną).
  • Referencje do obiektów w zamknięciu są przechowywane nawet po wyjściu z funkcji zewnętrznej, co pozwala na wdrażanie fabryk funkcji, leksykalnych liczników i wielu innych.

Pytania z podstępem.

Czy można zmieniać wartość zamkniętej zmiennej wewnątrz funkcji bez nonlocal?

Nie. Jeśli spróbujesz przypisać nową wartość, nie używając nonlocal, Python traktuje to jako tworzenie nowej lokalnej zmiennej, a stara wartość nie wyjdzie na zewnątrz.

Przykład:

def make_counter(): count = 0 def inner(): count += 1 # Błąd UnboundLocalError! return count return inner

Czy można przekazywać argumenty do zewnętrznego scope przez zamknięcie?

Tak, zamknięcie "zapamięta" wszelkie zmienne dostępne w zewnętrznej przestrzeni widzenia, w tym atrybuty klas, zmienne globalne itp. Ale zmiana tych zmiennych wymaga szczególnych wysiłków (np. użycia nonlocal lub global).

Co się dzieje z obiektami mutowalnymi wewnątrz zamknięcia?

Jeśli zamknięta zmienna jest referencją do obiektu mutowalnego, na przykład listy, możesz zmodyfikować jego zawartość bez nonlocal, ale jeśli spróbujesz przypisać zmienną — będzie potrzebne nonlocal.

Przykład:

def make_appender(): result = [] def append(x): result.append(x) # Można! return result return append f = make_appender() print(f(1)) # [1] print(f(2)) # [1, 2]

Typowe błędy i antywzorce

  • Próba przypisania zmiennej w zamknięciu bez nonlocal.
  • Użycie zamknięcia do przechowywania mutowalnego stanu, nie zdając sobie sprawy z możliwych wycieków pamięci.
  • Trudno czytelny kod z powodu zbyt dużej liczby zamkniętych zmiennych.

Przykład z życia

Negatywny przypadek

Programista pisze zamknięcie, zmienia zmienną bez nonlocal — pojawia się UnboundLocalError.

Zalety:

  • Szybkie prototypowanie liczników, fabryk.

Wady:

  • Nieprzewidywalne zachowanie, błędy związane z nonlocal.

Pozytywny przypadek

Wyraźne użycie nonlocal dla kontrolowanego stanu w zamknięciu.

Zalety:

  • Wyraźnie kontrolowany stan, łatwo wdrożyć liczniki i fabryki funkcji.

Wady:

  • Trudniejsza do zrozumienia i debugowania dużych łańcuchów zamknięć.