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ą.
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.
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 toKilka 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.
@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
wrapperzamiast 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_cachei podobne narzędzia używają tożsamości funkcji do kluczowania cache — bezwrapsmożesz uzyskać nieoczekiwane kolizje w cache.
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.' ← preservedfunctools.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:
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.
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.
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:
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:
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 ValueErrorDekorator @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:
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.
@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# 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.@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.