Szybki quiz – przypomnienie poprzedniej lekcji
Pytanie 1. Poniższy program uruchamia się bez żadnego komunikatu błędu. Jaki rodzaj błędu zawiera i dlaczego trudno go wykryć?
def srednia(liczby):
return sum(liczby) / len(liczby) + 1
print(srednia([2, 4]))
Odpowiedź
Odpowiedź: To błąd logiczny. Program działa, ale wypisuje 4.0 zamiast 3.0. Python nie zgłasza wyjątku, więc trzeba sprawdzić wynik samodzielnie, np. printem, debuggerem lub testem.
Pytanie 2. Przeanalizuj komunikat błędu. Od której linii zaczynasz czytanie, jaki to typ błędu i w której linii pliku program.py został on zgłoszony?
Traceback (most recent call last):
File "program.py", line 10, in <module>
wynik = oblicz(5, 0)
File "program.py", line 6, in oblicz
return a / b
ZeroDivisionError: division by zero
Odpowiedź
Odpowiedź: Czytamy od dołu. Ostatnia linia podaje typ błędu (ZeroDivisionError). Błąd zgłoszono w linii 6, w funkcji oblicz. Wywołano ją z linii 10.
Pytanie 3. Jaki wyjątek zgłosi każdy z tych fragmentów?
# a)
lista = [1, 2, 3]
print(lista[3])
# b)
slownik = {"a": 1}
print(slownik["b"])
# c)
print("5" + 5)
# d)
lista.add(4)
Odpowiedź
Odpowiedź: a) IndexError, b) KeyError, c) TypeError, d) AttributeError.
Pytanie 4. Co wyświetli ten program?
def podziel(a, b):
assert b != 0, "Dzielnik nie może być zerem!"
return a / b
print(podziel(10, 2))
print(podziel(10, 0))
Odpowiedź
Odpowiedź: Najpierw 5.0. Drugie wywołanie przerwie program błędem AssertionError: Dzielnik nie może być zerem!.
Pytanie 5. Zatrzymałeś program w pdb na linii, w której wywoływana jest funkcja. Którą komendą wejdziesz do środka tej funkcji, a którą przejdziesz do następnej linii bez wchodzenia? Jakie klawisze robią to samo w VS Code?
Odpowiedź
Odpowiedź: s (step) wchodzi do funkcji, n (next) przechodzi do następnej linii. W VS Code to odpowiednio F11 (step into) i F10 (step over).
Pytanie 6. Dobierz poziom logowania (DEBUG, INFO, ERROR) do komunikatów:
a) "Suma: 60" (potwierdzenie, że wszystko działa),
b) "Lista jest pusta!" (poważny problem),
c) "Wywołano oblicz_srednia z [10, 20, 30]" (szczegół diagnostyczny).
Odpowiedź
Odpowiedź: a) INFO, b) ERROR, c) DEBUG.
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:
stworz_powitanie("Hello")tworzy funkcjępowitajktóra „pamięta”pozdrowienie = "Hello"- 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ą - Z
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
- Napisz closure zwracające funkcję mnożącą przez dowolną liczbę.
- Napisz closure tworzące licznik (increment, decrement, reset).
- Napisz closure zapisujące historię wywołań funkcji.
- Stwórz funkcję fabrykującą validatory (min długość, max długość).
- Napisz closure implementujące prostą cache (memoizację).
- Stwórz closure z prywatnym stanem (np. konto bankowe).
- Napisz closure logujące wywołania funkcji.
- Stwórz closure obliczające średnią ruchomą.
- Napisz closure rate limitera (max wywołań w czasie).
- Stwórz closure generujące unikalne ID.
PODSUMOWANIE
- Closure to funkcja wewnętrzna, która zapamiętuje zmienne z funkcji zewnętrznej, nawet gdy funkcja zewnętrzna już się zakończyła.
- Closure powstaje, gdy spełnione są trzy warunki:
- funkcja jest zagnieżdżona,
- funkcja wewnętrzna używa zmiennej z funkcji zewnętrznej,
- funkcja zewnętrzna zwraca funkcję wewnętrzną.
- Zwracamy samą funkcję, bez nawiasów:
return wewnetrzna. Zapisreturn wewnetrzna()zwróciłby wynik wywołania, a nie funkcję. - Każde wywołanie funkcji zewnętrznej tworzy osobny, niezależny closure z własnymi zapamiętanymi wartościami (
razy_2irazy_5się nie mieszają). - Samo odczytywanie zmiennej zewnętrznej nie wymaga niczego dodatkowego. Do zmiany jej wartości (
+=,-=,=) potrzebne jestnonlocal. - Bez
nonlocalinstrukcja typucount += 1w funkcji wewnętrznej kończy się błędemUnboundLocalError. - Kilka funkcji zwróconych z jednego wywołania (np.
wplata,wyplata,saldow koncie) współdzieli te same zmienne zewnętrzne. - Zapamiętany stan jest “prywatny”: nie ma do niego bezpośredniego dostępu spoza funkcji. Closure to więc enkapsulacja bez klas.
- Closure sprawdza się w prostych przypadkach (jedna lub kilka funkcji ze stanem). Gdy potrzeba wielu metod i dziedziczenia, lepsza jest klasa.
- Dekoratory, memoizacja i handlery korzystają z closures:
wrapperzapamiętujefunkcjaprzekazaną do dekoratora.
Minimum, które powinno wejść w pamięć
Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:
- Closure = funkcja zagnieżdżona + używa zmiennej z zewnątrz + jest zwracana.
- Zwracasz funkcję bez nawiasów:
return wewnetrzna. - Każde wywołanie fabryki tworzy osobny closure z własnym stanem.
- Zmieniasz zmienną zewnętrzną? Napisz
nonlocal nazwa. Tylko ją czytasz?nonlocalnie jest potrzebne. - Brak
nonlocalprzy+=dajeUnboundLocalError. - Funkcje zwrócone razem (np. w słowniku) współdzielą zmienne zewnętrzne.
@dekoratornad funkcją działa jakfunkcja = dekorator(funkcja). Dekorator jest closure z funkcjąwrapper.- Closure ma sens przy prostym stanie. Przy wielu metodach i dziedziczeniu używamy klasy.
Ściągawka – najważniejsze polecenia i składnia
Fabryka funkcji (bez stanu):
def fabryka(parametr):
def wewnetrzna(x):
return x * parametr # używa zmiennej z zewnątrz
return wewnetrzna # bez nawiasów!
razy_2 = fabryka(2)
print(razy_2(10)) # 20
Closure ze stanem (nonlocal):
def licznik(start=0):
count = start
def increment():
nonlocal count # bez tego: UnboundLocalError
count += 1
return count
return increment
Kilka funkcji ze wspólnym stanem:
def stworz_cos():
stan = 0
def f1(): ...
def f2(): ...
return {"f1": f1, "f2": f2} # użycie: obiekt["f1"]()
Dekorator (schemat):
def dekorator(funkcja):
def wrapper(*args, **kwargs):
wynik = funkcja(*args, **kwargs)
return wynik
return wrapper
@dekorator
def moja(): ...
Inspekcja closure:
f.__closure__ # krotka komórek zapamiętanych zmiennych
f.__closure__[0].cell_contents # wartość pierwszej zapamiętanej zmiennej
Typowe błędy:
| Błąd w kodzie | Skutek |
|---|---|
return wewnetrzna() zamiast return wewnetrzna | zwracany jest wynik, a nie funkcja (potem TypeError: ... is not callable) |
brak nonlocal przy += / -= | UnboundLocalError |
brak return wewnetrzna | fabryka zwraca None |
wywołanie fabryka(2, 10) zamiast fabryka(2)(10) | TypeError (zła liczba argumentów) |
Quiz – sprawdź się
Pytanie 1. (teoria) Wymień trzy warunki, które musi spełniać closure. Kiedy zamiast closure lepiej użyć klasy?
Pytanie 2. (co zostanie wyświetlone?)
def stworz_mnoznik(n):
def mnoz(x):
return x * n
return mnoz
razy_3 = stworz_mnoznik(3)
razy_4 = stworz_mnoznik(4)
print(razy_3(5), razy_4(5))
Pytanie 3. (co zostanie wyświetlone?)
def licznik(start=0):
count = start
def increment():
nonlocal count
count += 1
return count
return increment
a = licznik()
b = licznik(10)
print(a(), a(), b(), a())
Pytanie 4. (dlaczego ten kod nie działa?) Jaki błąd zgłosi program i jak go naprawić?
def licznik():
count = 0
def increment():
count += 1
return count
return increment
c = licznik()
print(c())
Pytanie 5. (znajdź błąd) Program kończy się błędem w ostatniej linii. Wskaż przyczynę i popraw jedną linię.
def stworz_powitanie(pozdrowienie):
def powitaj(imie):
return f"{pozdrowienie}, {imie}!"
return powitaj("Anna")
hello = stworz_powitanie("Hello")
print(hello("Jan"))
Pytanie 6. (uzupełnij kod) Co wpisać w miejsce ____, aby program wypisał 25?
def stworz_potege(wykladnik):
def poteguj(podstawa):
return ____ ** ____
return ____
kwadrat = stworz_potege(2)
print(kwadrat(5))
Pytanie 7. (która wersja jest poprawna?) Chcesz obliczyć 5 do potęgi 3 za pomocą funkcji stworz_potege z pytania 6. Która wersja da wynik 125?
# A
print(stworz_potege(3)(5))
# B
print(stworz_potege(5)(3))
# C
print(stworz_potege(5, 3))
Pytanie 8. (przewidywanie działania programu) Co wyświetli ten program? Dlaczego stan() “widzi” zmiany wprowadzone przez wplata?
def stworz_konto(saldo):
def wplata(kwota):
nonlocal saldo
saldo += kwota
return saldo
def stan():
return saldo
return wplata, stan
wplata, stan = stworz_konto(100)
wplata(50)
wplata(25)
print(stan())
Pytanie 9. (co zostanie wyświetlone?) Wskaż, która funkcja jest tu closure i co ona zapamiętuje.
def glosno(funkcja):
def wrapper(*args, **kwargs):
wynik = funkcja(*args, **kwargs)
return wynik.upper()
return wrapper
@glosno
def powitanie(imie):
return f"cześć, {imie}"
print(powitanie("ola"))
Pytanie 10. (zadanie praktyczne) Napisz funkcję stworz_rabat(procent), która zwraca funkcję przyjmującą cenę i zwracającą cenę po obniżce o procent procent. Sprawdź ją tak:
rabat_20 = stworz_rabat(20)
print(rabat_20(100)) # oczekiwany wynik: 80.0
Odpowiedzi
1. Closure musi spełniać trzy warunki: funkcja jest zagnieżdżona, funkcja wewnętrzna używa zmiennych z funkcji zewnętrznej, funkcja zewnętrzna zwraca funkcję wewnętrzną. Klasa jest lepsza przy złożonych obiektach, wielu metodach i dziedziczeniu.
2. 15 20. Każde wywołanie stworz_mnoznik tworzy osobny closure z własnym n.
3. 1 2 11 3. a i b mają niezależne liczniki. Trzecie wywołanie a() zwraca 3, bo a pamięta własny count.
4. UnboundLocalError. Przy count += 1 Python traktuje count jako zmienną lokalną funkcji increment, której jeszcze nie ma. Naprawa: dodać nonlocal count jako pierwszą linię w increment.
5. Funkcja stworz_powitanie zwraca powitaj("Anna"), czyli tekst "Hello, Anna!", a nie funkcję. Wtedy hello("Jan") to próba wywołania tekstu: TypeError: 'str' object is not callable. Poprawka: return powitaj (bez nawiasów).
6.
return podstawa ** wykladnik
return poteguj
7. Wersja A. stworz_potege(3) ustawia wykładnik 3, a (5) podaje podstawę, więc wynik to 5³ = 125. W wersji B wynik to 3⁵ = 243. W wersji C jest za dużo argumentów (TypeError).
8. Wyświetli 175. Obie funkcje, wplata i stan, korzystają z tej samej zmiennej saldo z jednego wywołania stworz_konto. Stąd stan() zwraca wartość zmienioną przez wplata.
9. Wyświetli CZEŚĆ, OLA. Closure to wrapper. Zapamiętuje zmienną funkcja, czyli oryginalne powitanie przekazane do dekoratora.
10. Przykładowe rozwiązanie:
def stworz_rabat(procent):
def oblicz(cena):
return cena - cena * procent / 100
return oblicz
Dla rabat_20(100) wynik to 80.0.