To właśnie esencja elastycznego kodu: możesz zmieniać zachowanie programu bez ruszania jego wnętrzności. Dekorator działa jak filtr — opakowuje Twoją funkcję w dodatkową warstwę logiki, zachowując jej oryginalne działanie. W efekcie kod staje się nie tylko czystszy, ale też bardziej odporny na błędy i łatwiejszy do testowania.
Dobrze zaprojektowany dekorator to taki, który możesz zaaplikować jednym wierszem @nazwa_dekoratora, a on zrobi za Ciebie całą „brudną robotę” — od logowania po obsługę wyjątków.
Większość programistów zaczyna od pisania prostych funkcji. Potem poznaje klasy, obiekty i metody. Ale wcześniej czy później przychodzi moment, gdy chcesz dodać coś do zachowania funkcji — np. logowanie, pomiar czasu, autoryzację.
Nie chcesz jednak powtarzać tego samego kodu w wielu miejscach. Wtedy właśnie wchodzą one: dekoratory.
1. Czym jest dekorator?
W najprostszej formie, dekorator to funkcja, która przyjmuje inną funkcję jako argument i zwraca nową funkcję.
To pozwala „opakować" oryginalną funkcję w dodatkowe zachowanie, bez jej modyfikowania.
Python1 2 3 4 5 6 7 8 9 10 11 12def decorator(func): def wrapper(): print("Zanim wywołamy funkcję...") func() print("...a teraz po niej.") return wrapper def say_hello(): print("Cześć!") decorated = decorator(say_hello) decorated()
Wynik:
Bash1 2 3Zanim wywołamy funkcję... Cześć! ...a teraz po niej.
Dekorator nadaje funkcji nowe zachowanie – i właśnie w tym tkwi jego siła.
2. Składnia @ – syntactic sugar
Python wprowadza specjalny skrót do używania dekoratorów.
Zamiast pisać:
Python1say_hello = decorator(say_hello)
możesz po prostu napisać:
Python1 2 3@decorator def say_hello(): print("Cześć!")
To jest tylko skrót — ale sprawia, że kod jest czystszy i bardziej deklaratywny.
3. Dekoratory z argumentami funkcji
Większość funkcji nie jest bezargumentowa, więc musisz napisać dekorator, który potrafi przekazać argumenty dalej.
Python1 2 3 4 5 6 7 8 9 10 11 12 13def debug(func): def wrapper(*args, **kwargs): print(f"Wywołano {func.__name__} z argumentami {args} {kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} zwróciło {result}") return result return wrapper @debug def add(a, b): return a + b add(2, 3)
Ten dekorator jest uniwersalny — zadziała z dowolną funkcją, niezależnie od liczby argumentów.
4. Zastosowania dekoratorów w praktyce
1. Logowanie
Python1 2 3 4 5 6 7 8 9def log(func): def wrapper(*args, **kwargs): print(f"Funkcja {func.__name__} została wywołana") return func(*args, **kwargs) return wrapper @log def process(): print("Przetwarzanie danych...")
2. Pomiar czasu wykonania
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16import time def measure_time(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} wykonano w {end - start:.4f}s") return result return wrapper @measure_time def slow_function(): time.sleep(1) slow_function()
3. Autoryzacja użytkownika
Python1 2 3 4 5 6 7 8 9 10 11 12def require_admin(func): def wrapper(user): if not user.get("is_admin"): raise PermissionError("Brak uprawnień!") return func(user) return wrapper @require_admin def delete_user(user): print("Użytkownik usunięty!") delete_user({"is_admin": True})
4. Retry – ponawianie operacji
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25import time from functools import wraps def retry(max_attempts=3, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts - 1: raise print(f"Próba {attempt + 1} nie powiodła się: {e}") time.sleep(delay) return wrapper return decorator @retry(max_attempts=3, delay=2) def unreliable_api_call(): # symulacja niestabilnego połączenia import random if random.random() > 0.5: return "Sukces!" raise ConnectionError("Błąd połączenia")
5. Dekoratory z argumentami (metapoziom)
Co, jeśli chcesz przekazać parametr do dekoratora?
Wtedy potrzebujesz funkcji zwracającej dekorator.
Python1 2 3 4 5 6 7 8 9 10 11 12 13def repeat(n): def decorator(func): def wrapper(*args, **kwargs): for _ in range(n): func(*args, **kwargs) return wrapper return decorator @repeat(3) def greet(): print("Hej!") greet()
Dekorator repeat(3) wykonuje funkcję trzy razy.
To tzw. fabryka dekoratorów — funkcja, która tworzy dekorator na podstawie argumentu.
6. Zachowanie metadanych funkcji (functools.wraps)
Bez dodatkowych działań, dekorator nadpisuje nazwę i docstring funkcji.
Żeby tego uniknąć, użyj functools.wraps.
Python1 2 3 4 5 6 7 8from functools import wraps def debug(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Wywołano {func.__name__}") return func(*args, **kwargs) return wrapper
Dzięki @wraps funkcja zachowuje swoje metadane:
Python1 2print(add.__name__) # add, a nie wrapper print(add.__doc__) # zachowuje oryginalny docstring
To ważne przy debugowaniu, dokumentowaniu i testowaniu.
7. Dekoratory klasowe
Dekoratory nie ograniczają się do funkcji — możesz ich używać również do modyfikowania klas.
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17def singleton(cls): instances = {} def get_instance(): if cls not in instances: instances[cls] = cls() return instances[cls] return get_instance @singleton class Config: pass a = Config() b = Config() print(a is b) # True
Tutaj dekorator zapewnia, że klasa ma tylko jedną instancję — klasyczny wzorzec Singleton.
8. Wbudowane dekoratory w Pythonie
Python sam dostarcza kilka dekoratorów, które używasz codziennie:
@staticmethod— tworzy metodę statyczną w klasie,@classmethod— przekazuje klasę jako pierwszy argument,@property— pozwala definiować właściwości jak atrybuty.
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19class User: def __init__(self, name): self._name = name @property def name(self): return self._name.title() @staticmethod def is_valid_email(email): return "@" in email @classmethod def from_dict(cls, data): return cls(data["name"]) user = User("jan") print(user.name) # Jan print(User.is_valid_email("test@example.com")) # True
9. Stacking dekoratorów – łączenie wielu dekoratorów
Możesz użyć wielu dekoratorów naraz. Zastosowują się od dołu do góry.
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27from functools import wraps import time def log(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Log: {func.__name__} wywołana") return func(*args, **kwargs) return wrapper def timing(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"Timing: {func.__name__} trwała {end - start:.4f}s") return result return wrapper @log @timing def complex_calculation(): time.sleep(0.5) return 42 complex_calculation()
To jest równoważne z:
Python1complex_calculation = log(timing(complex_calculation))
10. Kiedy używać dekoratorów?
Używaj ich, gdy:
- chcesz dodać wspólne zachowanie do wielu funkcji (np. logowanie, walidacja, caching),
- chcesz oddzielić logikę biznesową od logiki technicznej (np. pomiar czasu, retry),
- zależy Ci na czystym, powtarzalnym kodzie.
Nie używaj ich, gdy:
- logika staje się trudna do zrozumienia,
- potrzebujesz dynamicznego sterowania przepływem w runtime (tam lepsze są klasy lub kontekst menedżery).
11. Ćwiczenie praktyczne – dekorator cache
Oto implementacja prostego dekoratora @cache, który zapamiętuje wyniki funkcji:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28from functools import wraps def cache(func): cache_dict = {} @wraps(func) def wrapper(*args, **kwargs): # Tworzenie klucza z argumentów key = str(args) + str(sorted(kwargs.items())) if key not in cache_dict: cache_dict[key] = func(*args, **kwargs) print(f"Obliczono i zapisano wynik dla {func.__name__}") else: print(f"Zwracam wynik z cache dla {func.__name__}") return cache_dict[key] return wrapper @cache def expensive_calculation(n): # symulacja czasochłonnego obliczenia result = sum(i ** 2 for i in range(n)) return result print(expensive_calculation(1000)) # Obliczono i zapisano print(expensive_calculation(1000)) # Zwracam wynik z cache
Uwaga: W rzeczywistych projektach użyj wbudowanego @functools.lru_cache, który jest bardziej zaawansowany i zoptymalizowany.
12. Podsumowanie
Dekoratory to jedno z najpotężniejszych narzędzi Pythona.
Pozwalają na pisanie kodu, który jest:
- bardziej modułowy (łatwiej oddzielić logikę),
- bardziej czytelny (mniej duplikacji),
- bardziej elastyczny (możesz rozszerzać zachowanie bez zmian w oryginalnym kodzie).
Zrozumienie dekoratorów to krok w stronę głębokiego opanowania Pythona.
To moment, gdy przestajesz pisać kod — a zaczynasz projektować zachowania.
Zadanie dla Ciebie:
Spróbuj zaimplementować własny dekorator @validate, który sprawdza, czy argumenty funkcji są liczbami dodatnimi. Jeśli nie są, powinien rzucić ValueError.
To świetne ćwiczenie na zrozumienie, jak funkcje mogą walidować dane wejściowe w sposób przejrzysty i powtarzalny.



