22. Python – Closures (Domknięcia)


Closure (domknięcie) to funkcja, która „zapamiętuje” zmienne z zakresu zewnętrznego, nawet po zakończeniu wykonywania funkcji zewnętrznej.

Kluczowe elementy:

  • Funkcja zagnieżdżona (wewnątrz innej funkcji)
  • Funkcja wewnętrzna odwołuje się do zmiennych zewnętrznych
  • Funkcja zewnętrzna zwraca funkcję wewnętrzną

Dlaczego closures są ważne?

  • Enkapsulacja danych
  • Tworzenie funkcji fabryk
  • Dekoratory
  • Callbacks i event handlery

Podstawy – funkcje zagnieżdżone

def zewnetrzna():
    """Funkcja zewnętrzna"""
    wiadomosc = "Hello"
    
    def wewnetrzna():
        """Funkcja wewnętrzna"""
        print(wiadomosc)  # Dostęp do zmiennej zewnętrznej
    
    wewnetrzna()

zewnetrzna()  # Hello

Zwracanie funkcji wewnętrznej

def zewnetrzna():
    wiadomosc = "Hello"
    
    def wewnetrzna():
        print(wiadomosc)
    
    return wewnetrzna  # Zwracamy funkcję (bez nawiasów!)

# Przypisz funkcję do zmiennej
funkcja = zewnetrzna()

# Wywołaj
funkcja()  # Hello

Ważne: Funkcja wewnetrzna „zapamiętała” zmienną wiadomosc mimo że zewnetrzna() już się zakończyła!

Pierwszy prawdziwy closure

def stworz_powitanie(pozdrowienie):
    """Funkcja fabrykująca - tworzy spersonalizowane powitania"""
    
    def powitaj(imie):
        return f"{pozdrowienie}, {imie}!"
    
    return powitaj

# Stwórz różne funkcje powitań
hello = stworz_powitanie("Hello")
czesc = stworz_powitanie("Cześć")
hola = stworz_powitanie("Hola")

# Użycie
print(hello("Anna"))   # Hello, Anna!
print(czesc("Jan"))    # Cześć, Jan!
print(hola("Maria"))   # Hola, Maria!

Co się dzieje:

  1. stworz_powitanie("Hello") tworzy funkcję powitaj która „pamięta” pozdrowienie = "Hello"
  2. Każde wywołanie tworzy niezależne closure z własną wartością

Closure z wieloma zmiennymi

def licznik(start=0):
    """Tworzy licznik zaczynający od start"""
    count = start
    
    def increment():
        nonlocal count  # Pozwala modyfikować zmienną zewnętrzną
        count += 1
        return count
    
    return increment

# Stwórz liczniki
licznik1 = licznik(0)
licznik2 = licznik(100)

print(licznik1())  # 1
print(licznik1())  # 2
print(licznik1())  # 3

print(licznik2())  # 101
print(licznik2())  # 102

Kluczowe słowo nonlocal:

  • Bez nonlocal – Python tworzy nową lokalną zmienną
  • nonlocal – modyfikuje zmienną z zakresu zewnętrznego

Przykład: Mnożnik

def stworz_mnoznik(n):
    """Tworzy funkcję mnożącą przez n"""
    
    def mnoz(x):
        return x * n
    
    return mnoz

# Stwórz różne mnożniki
razy_2 = stworz_mnoznik(2)
razy_5 = stworz_mnoznik(5)
razy_10 = stworz_mnoznik(10)

print(razy_2(10))   # 20
print(razy_5(10))   # 50
print(razy_10(10))  # 100

Przykład: Funkcja potęgująca

def stworz_potege(wykladnik):
    """Tworzy funkcję podnoszącą do potęgi"""
    
    def poteguj(podstawa):
        return podstawa ** wykladnik
    
    return poteguj

# Różne potęgi
kwadrat = stworz_potege(2)
szescian = stworz_potege(3)
czwarta = stworz_potege(4)

print(kwadrat(5))   # 25
print(szescian(5))  # 125
print(czwarta(2))   # 16

Closure ze stanem

def stworz_konto(wlasciciel, saldo_poczatkowe=0):
    """Tworzy konto bankowe z enkapsulacją"""
    saldo = saldo_poczatkowe
    
    def wplata(kwota):
        nonlocal saldo
        if kwota > 0:
            saldo += kwota
            return f"{wlasciciel}: Wpłata {kwota}. Saldo: {saldo}"
        return "Kwota musi być dodatnia"
    
    def wyplata(kwota):
        nonlocal saldo
        if kwota > saldo:
            return f"{wlasciciel}: Brak środków. Saldo: {saldo}"
        if kwota > 0:
            saldo -= kwota
            return f"{wlasciciel}: Wypłata {kwota}. Saldo: {saldo}"
        return "Kwota musi być dodatnia"
    
    def sprawdz_saldo():
        return f"{wlasciciel}: Saldo: {saldo}"
    
    # Zwróć słownik funkcji
    return {
        'wplata': wplata,
        'wyplata': wyplata,
        'saldo': sprawdz_saldo
    }

# Stwórz konta
konto_jan = stworz_konto("Jan", 1000)
konto_anna = stworz_konto("Anna", 500)

# Użycie
print(konto_jan['saldo']())        # Jan: Saldo: 1000
print(konto_jan['wplata'](500))    # Jan: Wpłata 500. Saldo: 1500
print(konto_jan['wyplata'](200))   # Jan: Wypłata 200. Saldo: 1300

print(konto_anna['saldo']())       # Anna: Saldo: 500

Zalety:

  • Dane są prywatne (nie można bezpośrednio modyfikować saldo)
  • Każde konto ma niezależny stan
  • Enkapsulacja bez klas

Closure vs Klasy

Z klasą:

class Licznik:
    def __init__(self, start=0):
        self.count = start
    
    def increment(self):
        self.count += 1
        return self.count

licznik = Licznik(0)
print(licznik.increment())  # 1

Z closure:

def licznik(start=0):
    count = start
    
    def increment():
        nonlocal count
        count += 1
        return count
    
    return increment

licznik = licznik(0)
print(licznik())  # 1

Kiedy używać:

  • Closure – proste przypadki, enkapsulacja jednej funkcji
  • Klasy – złożone obiekty, wiele metod, dziedziczenie

Praktyczne zastosowania

1. Dekoratory (najczęstsze!)

def mierz_czas(funkcja):
    """Dekorator mierzący czas wykonania"""
    import time
    
    def wrapper(*args, **kwargs):
        start = time.time()
        wynik = funkcja(*args, **kwargs)
        koniec = time.time()
        print(f"{funkcja.__name__} wykonała się w {koniec - start:.4f}s")
        return wynik
    
    return wrapper

@mierz_czas
def wolna_funkcja():
    import time
    time.sleep(1)
    return "Gotowe"

wynik = wolna_funkcja()
# wolna_funkcja wykonała się w 1.0012s

2. Funkcje fabrykujące

def stworz_validator(min_dlugosc, max_dlugosc):
    """Tworzy validator długości"""
    
    def waliduj(tekst):
        dlugosc = len(tekst)
        if dlugosc < min_dlugosc:
            return False, f"Za krótkie (min: {min_dlugosc})"
        if dlugosc > max_dlugosc:
            return False, f"Za długie (max: {max_dlugosc})"
        return True, "OK"
    
    return waliduj

# Stwórz różne validatory
waliduj_haslo = stworz_validator(8, 20)
waliduj_nick = stworz_validator(3, 15)

print(waliduj_haslo("abc"))      # (False, 'Za krótkie (min: 8)')
print(waliduj_haslo("Python123")) # (True, 'OK')
print(waliduj_nick("Jo"))        # (False, 'Za krótkie (min: 3)')

3. Callbacks z kontekstem

def stworz_button_handler(button_id):
    """Tworzy handler dla przycisku"""
    klikniecia = 0
    
    def on_click():
        nonlocal klikniecia
        klikniecia += 1
        print(f"Przycisk {button_id} kliknięty {klikniecia} razy")
    
    return on_click

# Stwórz handlery
button1_click = stworz_button_handler("Zapisz")
button2_click = stworz_button_handler("Anuluj")

# Symulacja kliknięć
button1_click()  # Przycisk Zapisz kliknięty 1 razy
button1_click()  # Przycisk Zapisz kliknięty 2 razy
button2_click()  # Przycisk Anuluj kliknięty 1 razy

4. Memoizacja (cache wyników)

def memoize(funkcja):
    """Closure cache'ujące wyniki funkcji"""
    cache = {}
    
    def wrapper(*args):
        if args in cache:
            print(f"Zwracam z cache dla {args}")
            return cache[args]
        
        print(f"Obliczam dla {args}")
        wynik = funkcja(*args)
        cache[args] = wynik
        return wynik
    
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(5))
# Obliczam dla (5,)
# Obliczam dla (4,)
# Obliczam dla (3,)
# Obliczam dla (2,)
# Obliczam dla (1,)
# Obliczam dla (0,)
# Zwracam z cache dla (1,)
# Zwracam z cache dla (2,)
# Zwracam z cache dla (3,)
# 5

5. Rate limiting

def rate_limiter(max_calls, time_window):
    """Ogranicza liczbę wywołań funkcji w czasie"""
    import time
    calls = []
    
    def decorator(funkcja):
        def wrapper(*args, **kwargs):
            nonlocal calls
            teraz = time.time()
            
            # Usuń stare wywołania
            calls = [c for c in calls if teraz - c < time_window]
            
            if len(calls) >= max_calls:
                return f"Za dużo wywołań! Max {max_calls} w {time_window}s"
            
            calls.append(teraz)
            return funkcja(*args, **kwargs)
        
        return wrapper
    return decorator

@rate_limiter(max_calls=3, time_window=10)
def wyslij_email(adres):
    return f"Email wysłany do {adres}"

# Pierwsze 3 wywołania - OK
print(wyslij_email("jan@test.com"))
print(wyslij_email("anna@test.com"))
print(wyslij_email("piotr@test.com"))

# 4-te - za dużo
print(wyslij_email("ewa@test.com"))
# Za dużo wywołań! Max 3 w 10s

Inspekcja closure

def zewnetrzna(x):
    def wewnetrzna(y):
        return x + y
    return wewnetrzna

closure_func = zewnetrzna(10)

# Sprawdź co closure "zapamiętała"
print(closure_func.__closure__)
# (<cell at 0x...: int object at 0x...>,)

# Wartość
print(closure_func.__closure__[0].cell_contents)  # 10

Podsumowanie Closures

Definicja:

  • Funkcja zagnieżdżona
  • Używa zmiennych z funkcji zewnętrznej
  • Jest zwracana przez funkcję zewnętrzną

Kluczowe słowa:

  • nonlocal – modyfikuj zmienną zewnętrzną

Zastosowania:

  • Dekoratory
  • Funkcje fabrykujące
  • Enkapsulacja danych
  • Callbacks z kontekstem
  • Memoizacja

Zalety:

  • Prywatne dane
  • Prostsza składnia niż klasy
  • Funkcyjne programowanie

Ćwiczenia

  1. Napisz closure zwracające funkcję mnożącą przez dowolną liczbę.
  2. Napisz closure tworzące licznik (increment, decrement, reset).
  3. Napisz closure zapisujące historię wywołań funkcji.
  4. Stwórz funkcję fabrykującą validatory (min długość, max długość).
  5. Napisz closure implementujące prostą cache (memoizację).
  6. Stwórz closure z prywatnym stanem (np. konto bankowe).
  7. Napisz closure logujące wywołania funkcji.
  8. Stwórz closure obliczające średnią ruchomą.
  9. Napisz closure rate limitera (max wywołań w czasie).
  10. Stwórz closure generujące unikalne ID.