Jeśli używałeś Flask lub FastAPI przez ponad tydzień, już używałeś dekoratorów — @app.route, @login_required, @pytest.mark.parametrize. Za pierwszym razem wyglądają jak magia, a potem ktoś wyjaśnia, co naprawdę się dzieje i od razu rozumiesz. Dekorator to po prostu funkcja, która owija inną funkcję. Składnia @ to czysty cukier syntaktyczny — @my_decorator nad definicją funkcji jest dokładnie równoważne z napisaniem func = my_decorator(func) po niej. To cały sekret. Reszta to tylko wzorce zbudowane na tej jednej idei. Ten artykuł buduje model mentalny od zera, a następnie omawia wzorce, których faktycznie użyjesz: @functools.wraps, dekoratory z argumentami, dekoratory klasowe i garść przykładów z prawdziwego życia, w tym @lru_cache, @dataclass i właściwy @retry z wykładniczym wycofaniem.

Funkcje są obiektami pierwszej klasy

Zanim dekoratory nabiorą sensu, musisz solidnie zrozumieć jeden fakt Pythona: funkcje są obiektami. Możesz przypisywać je do zmiennych, przekazywać jako argumenty do innych funkcji, zwracać z funkcji, i przechowywać na listach lub w słownikach. Nic szczególnego nie dzieje się tylko dlatego, że coś jest funkcją.

python
def greet(name: str) -> str:
    return f"Hello, {name}"

# Assign to a variable — no () means we're not calling it, just referencing it
say_hello = greet
print(say_hello("Alice"))   # 'Hello, Alice'

# Pass a function as an argument
def run_twice(fn, value):
    return fn(value), fn(value)

run_twice(greet, "Bob")     # ('Hello, Bob', 'Hello, Bob')

# Return a function from another function — this is a "factory"
def make_prefixer(prefix: str):
    def prefixed_greet(name: str) -> str:
        return f"{prefix}, {name}"
    return prefixed_greet

morning_hello = make_prefixer("Good morning")
morning_hello("Carol")      # 'Good morning, Carol'

Wewnętrzna funkcja prefixed_greet „zamyka się" nad zmienną prefix z otaczającego zakresu — nawet po tym, jak make_prefixer powróciło, wewnętrzna funkcja nadal ma dostęp do prefix. To jest domknięcie, i to właśnie mechanizm, który napędza dekoratory. Dokumentacja Pythona dotycząca zasad zakresu wyjaśnia to szczegółowo, jeśli chcesz pełny obraz.

Budowanie dekoratora od zera

Dekorator to funkcja, która przyjmuje funkcję i zwraca (zazwyczaj zmodyfikowaną) funkcję. Klasycznym pierwszym przykładem jest dekorator pomiaru czasu — owija dowolną funkcję i rejestruje, jak długo trwało jej uruchomienie.

python
import time
import functools

def timer(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{fn.__name__} finished in {elapsed:.4f}s")
        return result
    return wrapper

@timer
def fetch_user_records(db, user_id: int):
    """Fetch all records for a given user from the database."""
    return db.query("SELECT * FROM records WHERE user_id = ?", user_id)

# Calling fetch_user_records is now: timer(fetch_user_records)(db, 42)
# Which is exactly what @timer desugars to

Kilka rzeczy do zauważenia: wrapper używa *args, **kwargs, żeby móc przekazywać dowolną kombinację argumentów do oryginalnej funkcji bez znajomości jej sygnatury. Przechwytuje wartość zwracaną w result i ją zwraca, dzięki czemu opakowana funkcja zachowuje się identycznie z perspektywy wywołującego — ma tylko dodatkowy efekt uboczny w postaci drukowania. Usuń pomiaru czasu, a otrzymasz szkielet prawie każdego dekoratora, który kiedykolwiek napiszesz.

Zasada desugaring: Ilekroć widzisz @some_decorator nad definicją funkcji, mentalnie zamień to na fn = some_decorator(fn) napisane bezpośrednio po bloku def. Oba są dokładnie równoważne. Nie ma tu magii — to wywołanie funkcji.

Dlaczego musisz używać @functools.wraps

W powyższym przykładzie jest linia @functools.wraps(fn) na wrapperze. To nie jest opcjonalne. Bez tego twoja udekorowana funkcja traci swoją tożsamość — jej atrybuty __name__, __doc__ i __qualname__ zostają zastąpione tymi z wewnętrznej funkcji wrapper. Powoduje to subtelne błędy w kilku prawdziwych sytuacjach:

  • Docstringi znikają. help(fetch_user_records) pokazuje pusty docstring wrappera zamiast "Fetch all records for a given user...".
  • Ślady stosu kłamią. Gdy wewnątrz opakowanej funkcji zostanie wywołany wyjątek, traceback pokazuje wrapper zamiast prawdziwej nazwy funkcji — trudne do debugowania.
  • Introspekcja nie działa. Narzędzia takie jak pytest, system routingu Flask i inspect.signature() opierają się na __name__ i __wrapped__. Router Flask rzuci błędem, jeśli dwie trasy mają tę samą nazwę (wrappera).
  • functools.lru_cache i podobne narzędzia używają tożsamości funkcji do kluczowania cache — bez wraps możesz uzyskać nieoczekiwane kolizje w cache.
python
import functools

# Without @functools.wraps — broken
def bad_timer(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@bad_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'wrapper'   ← wrong
print(process_batch.__doc__)    # None         ← docstring gone

# With @functools.wraps — correct
def good_timer(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@good_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'process_batch'  ← correct
print(process_batch.__doc__)    # 'Process a single batch job.'  ← preserved

functools.wraps jest sam w sobie dekoratorem — kopiuje __module__, __name__, __qualname__, __annotations__, __doc__ i ustawia __wrapped__ na oryginalną funkcję. Używaj go na każdej funkcji wrapper, kropka. Nie ma powodu, żeby tego nie robić.

Dekoratory z argumentami

Zwykły dekorator przyjmuje funkcję i zwraca funkcję. Dekorator z argumentami potrzebuje jednego więcej poziomu: funkcji, która przyjmuje argumenty i zwraca dekorator. To trzy poziomy zagnieżdżenia, i prawie wszyscy mylą się przy pierwszym spotkaniu z nim. Oto dekorator @retry, który ponawia funkcję do max_attempts razy przy wyjątku:

python
import functools
import time

def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
    """
    Retry a function on exception with exponential backoff.

    Usage:
        @retry(max_attempts=5, delay=0.5, backoff=2.0)
        def call_external_api(endpoint: str) -> dict:
            ...
    """
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None

            for attempt in range(1, max_attempts + 1):
                try:
                    return fn(*args, **kwargs)
                except Exception as exc:
                    last_exception = exc
                    if attempt < max_attempts:
                        print(
                            f"{fn.__name__}: attempt {attempt}/{max_attempts} failed "
                            f"({exc!r}), retrying in {current_delay:.1f}s..."
                        )
                        time.sleep(current_delay)
                        current_delay *= backoff
                    else:
                        print(f"{fn.__name__}: all {max_attempts} attempts failed.")

            raise last_exception

        return wrapper
    return decorator

@retry(max_attempts=4, delay=0.5, backoff=2.0)
def fetch_price_data(ticker: str) -> dict:
    """Fetch stock price data from external API."""
    response = requests.get(f"https://api.example.com/prices/{ticker}", timeout=5)
    response.raise_for_status()
    return response.json()

Łańcuch wywołań, gdy Python przetwarza @retry(max_attempts=4, delay=0.5, backoff=2.0): najpierw wywołuje retry(max_attempts=4, delay=0.5, backoff=2.0) i zwraca decorator. Następnie wywołuje decorator(fetch_price_data) i zwraca wrapper. Na końcu fetch_price_data jest ponownie powiązana z wrapper. Więc @retry(...) to fetch_price_data = retry(...)(fetch_price_data) — trzy wywołania, dwa poziomy owijania. Gdy raz zobaczysz ten wzorzec, fabryki dekoratorów przestają być mylące.

Dekoratory klasowe

Możesz też implementować dekorator jako klasę, definiując __call__. Jest to przydatne, gdy dekorator musi utrzymywać stan między wywołaniami — licznik wywołań, cache, pule połączeń — ponieważ zmienne instancji są bardziej naturalnym miejscem dla tego stanu niż zmienne domknięcia.

python
import functools
import time

class RateLimiter:
    """
    Decorator that limits how often a function can be called.
    Raises RuntimeError if the function is called within `min_interval` seconds
    of the previous call.
    """

    def __init__(self, min_interval: float):
        self.min_interval = min_interval
        self._last_called: float = 0.0

    def __call__(self, fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            now = time.monotonic()
            since_last = now - self._last_called
            if since_last < self.min_interval:
                wait = self.min_interval - since_last
                raise RuntimeError(
                    f"{fn.__name__} called too soon — "
                    f"wait {wait:.2f}s before calling again."
                )
            self._last_called = now
            return fn(*args, **kwargs)
        return wrapper

# Usage — one call per 2 seconds
@RateLimiter(min_interval=2.0)
def send_sms_alert(phone: str, message: str) -> None:
    """Send an SMS alert via the gateway API."""
    sms_gateway.send(phone, message)

@RateLimiter(min_interval=2.0) działa dokładnie jak fabryka dekoratora: RateLimiter(2.0) tworzy instancję, a następnie ta instancja jest wywoływana z send_sms_alert, ponieważ ma __call__. Instancja przechowuje _last_called jako atrybut — żadnego żonglowania zmiennymi domknięcia. Zobacz PEP 318 (oryginalna propozycja dekoratorów) w celu uzyskania uzasadnienia projektu za składnią @, i PEP 614 dla rozluźnionej gramatyki dekoratorów, która pojawiła się w Pythonie 3.9.

Dekoratory z prawdziwego świata ze standardowej biblioteki

Zanim napiszesz własny, sprawdź, czy biblioteka standardowa nie ma już tego, czego potrzebujesz. Trzy dekoratory w functools pojawiają się stale w kodzie Pythona w środowiskach produkcyjnych.

python
from functools import lru_cache, cache
import requests

# @cache — unbounded memoisation (Python 3.9+)
# Caches every unique set of arguments forever.
# Good for pure functions with a small domain of inputs.
@cache
def get_country_name(country_code: str) -> str:
    """Look up a country name from ISO 3166 code. Cached after first call."""
    response = requests.get(f"https://restcountries.com/v3.1/alpha/{country_code}")
    return response.json()[0]["name"]["common"]

get_country_name("DE")   # hits the API
get_country_name("DE")   # served from cache, no network call

# @lru_cache(maxsize=N) — bounded LRU cache
# Evicts least-recently-used entries once the cache hits `maxsize`.
# Better when the input domain is large and memory is a concern.
@lru_cache(maxsize=256)
def compute_discount(base_price: float, tier: str) -> float:
    """Heavy computation — price varies by tier. Cache the top 256 combinations."""
    discount_table = load_discount_table()          # expensive DB call
    rate = discount_table.get(tier, 0.0)
    return round(base_price * (1 - rate), 2)

# Inspect cache performance
print(compute_discount.cache_info())
# CacheInfo(hits=142, misses=14, maxsize=256, currsize=14)

@dataclass jest w innej kategorii — to dekorator klasy, który automatycznie generuje __init__, __repr__ i __eq__ z twoich adnotacji pól. Eliminuje znaczną ilość kodu szablonowego dla klas przechowujących dane:

python
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime

@dataclass
class WebhookEvent:
    event_type: str
    source_id: int
    payload: dict
    received_at: datetime = field(default_factory=datetime.utcnow)
    retry_count: int = 0
    error_message: Optional[str] = None

# @dataclass generates all of this for free:
# - __init__(self, event_type, source_id, payload, received_at=..., retry_count=0, error_message=None)
# - __repr__ that shows all fields
# - __eq__ that compares field-by-field

event = WebhookEvent(event_type="order.created", source_id=9912, payload={"order_id": 44501})
print(event)
# WebhookEvent(event_type='order.created', source_id=9912, payload={...}, retry_count=0, ...)

Dekorator @property zamienia metodę w akcesor w stylu atrybutu — wywołujący czytają user.display_name zamiast user.get_display_name(). Połącz go z @property.setter, żeby walidować przy zapisie:

python
class UserProfile:
    def __init__(self, first_name: str, last_name: str, email: str):
        self._first_name = first_name
        self._last_name = last_name
        self._email = email.strip().lower()

    @property
    def display_name(self) -> str:
        return f"{self._first_name} {self._last_name}"

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self, value: str) -> None:
        if "@" not in value:
            raise ValueError(f"Invalid email address: {value!r}")
        self._email = value.strip().lower()

profile = UserProfile("Alice", "Smith", "  [email protected]  ")
print(profile.display_name)   # 'Alice Smith'
print(profile.email)          # '[email protected]'

profile.email = "[email protected]"    # setter runs validation
profile.email = "not-an-email"           # raises ValueError

Dekorator @require_auth dla obsługi tras

Sprawdzanie uwierzytelnienia w frameworkach webowych to podręcznikowy przypadek użycia dekoratorów. Zamiast powielać sprawdzenie „czy użytkownik jest zalogowany?" na początku każdego handlera trasy, piszesz je raz jako dekorator i stosujesz tam, gdzie potrzeba. Oto wzorzec, napisany do pracy z Flask, ale przenośny na każdy framework:

python
import functools
from flask import request, jsonify, g

def require_auth(fn):
    """
    Decorator that validates a Bearer token before the route handler runs.
    Sets g.current_user on success; returns 401 JSON on failure.
    """
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Missing or malformed Authorization header"}), 401

        token = auth_header[len("Bearer "):]
        user = verify_token(token)          # your token validation logic
        if user is None:
            return jsonify({"error": "Invalid or expired token"}), 401

        g.current_user = user               # available to the route handler
        return fn(*args, **kwargs)
    return wrapper

# Usage — the auth check runs before the handler body
@app.route("/api/v1/reports/<int:report_id>")
@require_auth
def get_report(report_id: int):
    report = Report.query.get_or_404(report_id)
    if report.owner_id != g.current_user.id:
        return jsonify({"error": "Forbidden"}), 403
    return jsonify(report.to_dict())

Zwróć uwagę na kolejność: @app.route idzie pierwsze (najbardziej zewnętrzne), @require_auth drugie. Ma to znaczenie — patrz następna sekcja. Wzorzec naturalnie się rozszerza: możesz dodać fabrykę dekoratora @require_role("admin"), która sprawdza g.current_user.role po tym jak @require_auth już zweryfikował, że użytkownik istnieje.

Nakładanie dekoratorów — kolejność ma znaczenie

Gdy nakładasz wiele dekoratorów, stosują się od dołu w górę (dekorator najbliżej funkcji stosuje się jako pierwszy), ale wykonują się od góry do dołu, gdy funkcja jest wywoływana. To łapie ludzi.

python
@decorator_a
@decorator_b
@decorator_c
def my_function():
    pass

# This is exactly equivalent to:
my_function = decorator_a(decorator_b(decorator_c(my_function)))

# When my_function() is called:
# 1. decorator_a's wrapper runs first (outermost)
# 2. decorator_b's wrapper runs second
# 3. decorator_c's wrapper runs third (innermost, closest to the real function)
# 4. The real my_function body runs
# 5. Unwinding: decorator_c → decorator_b → decorator_a
python
# Practical example: order matters for @timer and @retry
# If timer is outermost, it measures total time including retry wait periods.
# If retry is outermost, it measures only the final successful call.

@timer          # measures: total time across all retry attempts + sleep
@retry(max_attempts=3, delay=1.0)
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# vs.

@retry(max_attempts=3, delay=1.0)  # measures: only the final successful call
@timer
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# Usually you want @timer outermost — it tells you the real wall-clock cost of the operation.
# Think about what "calling this function" means to the caller, then wrap in that order.
Wskazówka dotycząca śladu stosu: Gdy udekorowana funkcja rzuca wyjątek, traceback pokaże każdy wrapper w stosie wywołań. Jeśli użyłeś @functools.wraps poprawnie, nazwy będą odzwierciedlać oryginalne funkcje. Jeśli widzisz mnóstwo ramek wrapper, ktoś zapomniał o @functools.wraps. Artykuł Real Python dotyczący dekoratorów zawiera dobre omówienie debugowania nakładanych dekoratorów.

Podsumowanie

Model mentalny do zapamiętania: @decorator to fn = decorator(fn). Wszystko inne — dekoratory z argumentami, dekoratory klasowe, nakładane dekoratory — to warianty tej jednej podmiany. Używaj @functools.wraps na każdym wewnętrznym wrapperze, zawsze przekazuj *args, **kwargs do opakowanej funkcji i zwracaj jej wynik. Dekoratory z argumentami potrzebują trzech poziomów zagnieżdżenia: funkcji argumentów, dekoratora i wrappera. Dekoratory klasowe są właściwym wyborem, gdy twój dekorator musi utrzymywać stan między wywołaniami.

Do dalszego czytania: wpis w słowniku Pythona dotyczący dekoratorów jest krótki, ale precyzyjny. Oryginalna propozycja dekoratora, PEP 318, warta jest przeczytania dla kontekstu, dlaczego składnia @ została wybrana zamiast alternatyw. Jeśli używasz dekoratorów w bazie kodu, która zajmuje się też dużo przetwarzaniem danych — czytaniem plików, transformowaniem rekordów — wzorce tutaj naturalne łączą się z tym, co omawiane jest w obsłudze plików Python. A jeśli używasz dekoratorów do transformacji kolekcji lub budowania struktur wyszukiwania, listy składane Python obejmują stronę transformacji danych tego obrazu. Jeśli twoje udekorowane funkcje zwracają wyniki JSON i chcesz je szybko zbadać, JSON Formatter na tej stronie jest do tego przydatny.