Om du har använt Flask eller FastAPI i mer än en vecka har du redan använt dekoratorer — @app.route,
@login_required, @pytest.mark.parametrize. De känns som magi första gången, och
sedan förklarar någon vad som faktiskt händer och det klickar direkt. En dekorator är bara en funktion som
omsluter en annan funktion. @-syntaxen är rent syntaktiskt socker — @my_decorator ovanför en
funktionsdefinition är exakt detsamma som att skriva func = my_decorator(func) efter den. Det är
hela hemligheten. Resten är bara mönster byggda ovanpå den enda idén. Den här artikeln bygger den mentala modellen från
grunden och går sedan igenom de mönster du faktiskt kommer att använda: @functools.wraps, dekoratorer med
argument, klassbaserade dekoratorer, och en handfull verkliga exempel inklusive @lru_cache,
@dataclass, och en ordentlig @retry med exponentiell backoff.
Funktioner är förstklassiga objekt
Innan dekoratorer ger mening måste du ha koll på ett Python-faktum: funktioner är objekt. Du kan tilldela dem till variabler, skicka dem som argument till andra funktioner, returnera dem från funktioner, och lagra dem i listor eller dictionaries. Inget speciellt händer bara för att något är en funktion.
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'Den inre funktionen prefixed_greet "stänger över" variabeln prefix från
det omslutande omfånget — även efter att make_prefixer har returnerat har den inre funktionen fortfarande tillgång
till prefix. Det här är en
closure,
och det är mekanismen som driver dekoratorer.
Pythons dokumentation om omfångsregler
förklarar detta i detalj om du vill ha hela bilden.
Bygga en dekorator från grunden
En dekorator är en funktion som tar en funktion och returnerar en (vanligtvis modifierad) funktion. Det klassiska första exemplet är en timing-dekorator — den omsluter vilken funktion som helst och loggar hur lång tid den tog att köra.
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 toNågra saker att notera: wrappern använder *args, **kwargs så att den kan vidarebefordra vilken
kombination av argument som helst till den ursprungliga funktionen utan att känna till dess signatur. Den fångar returvärdet
i result och returnerar det, så den omslutna funktionen beter sig identiskt från
anroparens perspektiv — den har bara en extra utskrift som bieffekt. Ta bort timingen, och du har
skelettet till nästan varje dekorator du någonsin kommer att skriva.
@some_decorator ovanför en
funktionsdefinition, byt mentalt ut det mot fn = some_decorator(fn) skrivet direkt
efter def-blocket. De två är exakt ekvivalenta. Det finns ingen magi — det är ett funktionsanrop.Varför du måste använda @functools.wraps
I exemplet ovan finns en rad @functools.wraps(fn) på wrappern. Det här är
inte valfritt. Utan det förlorar din dekorerade funktion sin identitet — dess __name__,
__doc__ och __qualname__-attribut ersätts alla med de från den inre
wrapper-funktionen. Det orsakar subtila fel i ett par verkliga situationer:
- Docstrings försvinner.
help(fetch_user_records)visar wrappers tomma docstring istället för"Fetch all records for a given user...". - Stack traces ljuger. När ett undantag uppstår inuti den omslutna funktionen visar traceback
wrapperistället för det riktiga funktionsnamnet — svårt att debugga. - Introspection går sönder. Verktyg som pytest, Flasks routingsystem och
inspect.signature()förlitar sig på__name__och__wrapped__. Flasks router kastar om två rutter delar samma (wrapper-) namn. functools.lru_cacheoch liknande verktyg använder funktionens identitet för cache-nycklar — utanwrapskan du få överraskande cache-kollisioner.
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
är i sig själv en dekorator — den kopierar __module__, __name__, __qualname__,
__annotations__, __doc__ och sätter __wrapped__ till den ursprungliga funktionen.
Använd den på varje wrapper-funktion, punkt. Det finns ingen anledning att inte göra det.
Dekoratorer med argument
En vanlig dekorator tar en funktion och returnerar en funktion. En dekorator med argument behöver en
nivå till: en funktion som tar argumenten och returnerar en dekorator. Det är tre nivåer av nästling,
och det förvirrar nästan alla första gången de ser det. Här är en @retry-dekorator som försöker om
en funktion upp till max_attempts gånger vid undantag:
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()Anropskedjan när Python bearbetar @retry(max_attempts=4, delay=0.5, backoff=2.0):
först anropas retry(max_attempts=4, delay=0.5, backoff=2.0) och returnerar decorator.
Sedan anropas decorator(fetch_price_data) och returnerar wrapper. Slutligen
binds fetch_price_data om till wrapper. Så @retry(...) är
fetch_price_data = retry(...)(fetch_price_data) — tre anrop, två nivåer av omslutning.
När du väl ser det mönstret slutar dekoratorfabriker att vara förvirrande.
Klassbaserade dekoratorer
Du kan också implementera en dekorator som en klass genom att definiera __call__. Det är användbart
när dekoratorn behöver upprätthålla tillstånd mellan anrop — en anropsräknare, ett cache, anslutningspooler — eftersom
instansvariabler är en mer naturlig plats för det tillståndet än closure-variabler.
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) fungerar precis som en dekoratorfabrik:
RateLimiter(2.0) konstruerar en instans, sedan anropas den instansen med
send_sms_alert eftersom den har __call__. Instansen lagrar
_last_called som ett attribut — inget jonglering med closure-variabler. Se
PEP 318 (det ursprungliga
dekoratorförslaget) för designmotiveringen bakom @-syntaxen, och
PEP 614 för avslappnad
dekoratogrammatik som landade i Python 3.9.
Verkliga dekoratorer från standardbiblioteket
Innan du skriver din egen, kontrollera om stdlib redan har det du behöver. Tre dekoratorer i
functools
dyker upp hela tiden i Python-kod i produktion.
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 är i en annan kategori — det är en klassdekorator som automatgenererar
__init__, __repr__ och __eq__ från dina fältannotationer. Det tar bort
en betydande mängd boilerplate för datahållande klasser:
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, ...)Dekoratorn @property omvandlar en metod till en attributliknande accessor — anropare
läser user.display_name istället för user.get_display_name(). Kombinera den med
@property.setter för att validera vid skrivning:
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 ValueErrorEn @require_auth-dekorator för route-hanterare
Autentiseringskontroller i webbramverk är ett läroboksfall för dekoratorer. Istället för att duplicera "är användaren inloggad?"-kontrollen i början av varje route-hanterare, skriver du den en gång som en dekorator och tillämpar den där det behövs. Här är mönstret, skrivet för att fungera med Flask men överförbart till vilket ramverk som helst:
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())Notera ordningen: @app.route går först (ytterst), @require_auth går
på andra plats. Det spelar roll — se nästa avsnitt. Mönstret utökas naturligt: du kan lägga till en
dekoratorfabrik @require_role("admin") som kontrollerar g.current_user.role efter
att @require_auth redan har verifierat att användaren finns.
Staplade dekoratorer — ordningen spelar roll
När du staplar flera dekoratorer tillämpas de nedifrån och upp (dekoratorn närmast funktionen tillämpas först), men de körs uppifrån och ned när funktionen anropas. Det här överraskar folk.
@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 korrekt återspeglar namnen
de ursprungliga funktionerna. Om du ser ett hav av wrapper-ramar glömde någon
@functools.wraps.
Real Python:s dekoratorporimer
har en bra genomgång av hur man debuggar staplade dekoratorer.Sammanfattning
Den mentala modellen att bära med sig: @decorator är fn = decorator(fn).
Allt annat — dekoratorer med argument, klassbaserade dekoratorer, staplade dekoratorer — är en variation på
den enda substitutionen. Använd @functools.wraps på varje inre wrapper, vidarebefordra alltid
*args, **kwargs till den omslutna funktionen och returnera dess resultat. Dekoratorer med argument behöver
tre nivåer av nästling: argumentfunktionen, dekoratorn och wrappern. Klassbaserade dekoratorer är
rätt val när din dekorator behöver upprätthålla tillstånd mellan anrop.
För vidare läsning: Pythons
glosarinlägg om dekoratorer
är kort men precist. Det ursprungliga dekoratorförslaget,
PEP 318, är värt att läsa
för kontexten kring varför @-syntaxen valdes framför alternativ. Om du använder dekoratorer
i en kodbas som också gör mycket databearbetning — läsning av filer, transformering av poster — passar mönstren
här naturligt ihop med det som täcks i
Python filhantering. Och om du använder dekoratorer för att
transformera samlingar eller bygga uppslagsstrukturer täcker
Python listförståelser datatransformationssidan av den bilden.
Om dina dekorerade funktioner returnerar JSON-utdata och du vill inspektera det snabbt är
JSON Formatter på den här sidan praktiskt för det.