Dekoratory w Pythonie – jak i kiedy używać

Kacper Sieradziński
Kacper Sieradziński
11 marca 2025Edukacja4 min czytania

Naucz się pisać funkcje, które modyfikują inne funkcje — potężne narzędzie do czystego, elastycznego i przewidywalnego kodu. Dekoratory pozwalają dodawać nowe zachowania do istniejących funkcji bez ich zmieniania. Dzięki nim możesz wprowadzać takie rzeczy jak logowanie, cache, autoryzacja czy pomiar czasu wykonania w sposób modułowy i wielokrotnego użytku. Zamiast kopiować ten sam kod w dziesięciu miejscach, piszesz jeden dekorator, który robi to raz — elegancko, bez duplikacji i z pełną kontrolą nad tym, kiedy i jak działa.

Obraz główny Dekoratory w Pythonie – jak i kiedy używać

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.

Python
1 2 3 4 5 6 7 8 9 10 11 12 def 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:

Bash
1 2 3 Zanim 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ć:

Python
1 say_hello = decorator(say_hello)

możesz po prostu napisać:

Python
1 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.

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 def 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

Python
1 2 3 4 5 6 7 8 9 def 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

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import 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

Python
1 2 3 4 5 6 7 8 9 10 11 12 def 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

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import 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.

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 def 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.

Python
1 2 3 4 5 6 7 8 from 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:

Python
1 2 print(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.

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def 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.
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class 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.

Python
1 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 from 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:

Python
1 complex_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:

Python
1 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 28 from 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.