programowanieBackend developer

Wyjaśnij, jak działają zamknięcia (closures) w Pythonie, czym różnią się od zwykłych funkcji i jakie mają praktyczne zastosowanie?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Historia pytania

Termin "zamknięcie" został zapożyczony z programowania funkcyjnego i pojawił się w Pythonie od samego początku. Zamknięcia pozwalają funkcjom zapamiętywać otoczenie, w którym zostały stworzone, nawet jeśli są wywoływane poza tym otoczeniem. Ta koncepcja zapewnia elastyczność i pozwala na realizację wielu wzorców, w tym fabryk funkcji i leniwych obliczeń.

Problem

W Pythonie funkcje są obiektami pierwszej klasy. Czasami zachodzi potrzeba, aby funkcja zagnieżdżona korzystała ze zmiennych z zakresu funkcji zewnętrznej, nawet po jej zakończeniu. Zwykły leksykalny zasięg tego nie gwarantuje przy zwracaniu funkcji. Jeśli taka funkcja odwołuje się do zmiennych otoczenia swojego utworzenia — powstaje zamknięcie.

Rozwiązanie

Zamknięcie powstaje, jeśli wewnętrzna funkcja odnosi się do zmiennych zdefiniowanych w zewnętrznej, a ta zewnętrzna zwraca wewnętrzną na zewnątrz. Jest to często wykorzystywane do tworzenia fabryk funkcji, enkapsulacji stanu bez klas oraz konstruowania funkcji z parametrami "na miejscu".

Przykład kodu:

def make_multiplier(factor): def multiplier(x): return x * factor return multiplier mul2 = make_multiplier(2) mul3 = make_multiplier(3) print(mul2(10)) # 20 print(mul3(10)) # 30

Kluczowe cechy:

  • Zamknięcie przechowuje wartości zmiennych otoczenia, nawet jeśli zewnętrzna funkcja już się zakończyła.
  • Stan w wewnętrznej funkcji jest w zasadzie prywatny, nie można go zmienić bezpośrednio z zewnątrz.
  • Aby zmienić zmienną nie-immutable zewnętrznej funkcji wewnątrz zamknięcia, używa się słowa kluczowego nonlocal.

Pytania z pułapką.

Czy zamknięcie może zachować zmienny stan między wywołaniami, jeśli zmienna jest zmieniana wewnątrz funkcji zagnieżdżonej?

Tak, jeśli wewnętrzna funkcja skorzysta ze słowa kluczowego nonlocal. Bez nonlocal przypisanie tworzy nową lokalną zmienną, nie zmieniając zewnętrznej.

def counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = counter() print(c()) # 1 print(c()) # 2

Czy można zrealizować prywatne zmienne w Pythonie, używając zamknięć, zamiast klas?

Tak, zamknięcie oferuje prostą implementację "prywatnych" zmiennych, niedostępnych z zewnątrz, jeśli nie są dostarczane gettery/settery wewnętrznej funkcji.

Czy zamknięcia stosowane są tylko do funkcji? Czy można zorganizować zamknięcie z lambdami w Pythonie?

Tak, zamknięcie może powstawać także z wyrażeń lambda, ponieważ są one podobne do def pod względem związania zmiennych leksykalnych.

def make_power(n): return lambda x: x ** n square = make_power(2) cube = make_power(3) print(square(4)) # 16 print(cube(2)) # 8

Typowe błędy i antywzorce

  • Oczekiwanie, że zamknięcie automatycznie zmienia zewnętrzną zmienną przy jej zmianie wewnątrz bez nonlocal.
  • Chwytanie obiektów zmiennych w zamknięciu i mutowanie ich, nie zdając sobie sprawy z trudności debugowania.
  • Używanie pętli do tworzenia funkcji w zamknięciu bez odpowiedniego związania zmiennych (pułapka "wszystkie funkcje widzą ostatnią wartość zmiennej").

Przykład z życia

Negatywny przypadek

Fabryka funkcji, która formuje obsługiwacze w pętli, używa zmiennej pętli wewnątrz zamknięcia:

handlers = [] for i in range(3): def handler(x): return x + i handlers.append(handler) print([h(10) for h in handlers]) # [12, 12, 12]

Zalety:

  • Prosto, mało kodu.

Wady:

  • Wszystkie obsługiwacze odnoszą się do tej samej zmiennej i, jej ostatnia wartość to 2 — zachowanie nieoczekiwane dla większości.

Pozytywny przypadek

Użyto argumentu domyślnego, aby "zablokować" wartość:

handlers = [] for i in range(3): def handler(x, j=i): return x + j handlers.append(handler) print([h(10) for h in handlers]) # [10, 11, 12]

Zalety:

  • Związanie potrzebnej wartości.
  • Przewidywalne zachowanie.

Wady:

  • Należy pamiętać o tej subtelności i ręcznie poprawiać zamknięcie, co komplikuje utrzymanie kodu.