Hvis du har brugt Flask eller FastAPI i mere end en uge, har du allerede brugt dekoratorer — @app.route, @login_required, @pytest.mark.parametrize. De føles som magi første gang, og derefter forklarer nogen hvad der faktisk sker, og det klikker med det samme. En dekorator er bare en funktion der omslutter en anden funktion. @-syntaksen er rent syntaktisk sukker — @my_decorator over en funktionsdefinition er præcis det samme som at skrive func = my_decorator(func) efter den. Det er hele hemmeligheden. Resten er bare mønstre bygget oven på den ene idé. Denne artikel bygger den mentale model fra bunden, og går derefter igennem de mønstre, du faktisk vil bruge: @functools.wraps, dekoratorer med argumenter, klassebaserede dekoratorer, og en håndfuld virkelige eksempler inklusive @lru_cache, @dataclass, og en ordentlig @retry med eksponentiel backoff.

Funktioner er førsteklasses objekter

Før dekoratorer giver mening, skal du have styr på et Python-faktum: funktioner er objekter. Du kan tildele dem til variabler, sende dem som argumenter til andre funktioner, returnere dem fra funktioner, og gemme dem i lister eller dicts. Intet specielt sker bare fordi noget er en funktion.

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'

Den indre funktion prefixed_greet "lukker over" variablen prefix fra det omsluttende omfang — selv efter at make_prefixer er returneret har den indre funktion stadig adgang til prefix. Det er en closure, og det er mekanismen der driver dekoratorer. Pythons dokumentation om omfangsregler forklarer dette i detaljer, hvis du vil have det fulde billede.

Bygge en dekorator fra bunden

En dekorator er en funktion der tager en funktion og returnerer en (normalt modificeret) funktion. Det klassiske første eksempel er en timing-dekorator — den omslutter enhver funktion og logger, hvor lang tid det tog at køre.

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

Et par ting at bemærke: wrapperen bruger *args, **kwargs så den kan videresende enhver kombination af argumenter til den originale funktion uden at kende dens signatur. Den fanger returværdien i result og returnerer den, så den omsluttede funktion opfører sig identisk fra kallerens perspektiv — den har bare en ekstra udskrivning som bieffekt. Fjern timingen, og du har skelettet af næsten enhver dekorator du nogensinde vil skrive.

Desugaring-reglen: Når du ser @some_decorator over en funktionsdefinition, erstat det mentalt med fn = some_decorator(fn) skrevet direkte efter def-blokken. De to er præcis ækvivalente. Der er ingen magi — det er et funktionskald.

Hvorfor du skal bruge @functools.wraps

I eksemplet ovenfor er der en @functools.wraps(fn)-linje på wrapperen. Det her er ikke valgfrit. Uden det mister din dekorerede funktion sin identitet — dens __name__, __doc__ og __qualname__-attributter erstattes alle med dem fra den indre wrapper-funktion. Det forårsager subtile fejl i et par virkelige situationer:

  • Docstrings forsvinder. help(fetch_user_records) viser wrapperens tomme docstring i stedet for "Fetch all records for a given user...".
  • Stack traces lyver. Når en undtagelse opstår inde i den omsluttede funktion, viser traceback wrapper i stedet for det rigtige funktionsnavn — svært at debugge.
  • Introspection går i stykker. Værktøjer som pytest, Flasks routingsystem og inspect.signature() er alle afhængige af __name__ og __wrapped__. Flasks router kaster, hvis to ruter deler det samme (wrapper-) navn.
  • functools.lru_cache og lignende værktøjer bruger funktionens identitet til cache-nøgler — uden wraps kan du få overraskende cache-kollisioner.
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 er i sig selv en dekorator — den kopierer __module__, __name__, __qualname__, __annotations__, __doc__ og sætter __wrapped__ til den originale funktion. Brug den på enhver wrapper-funktion, punktum. Der er ingen grund til ikke at gøre det.

Dekoratorer med argumenter

En almindelig dekorator tager en funktion og returnerer en funktion. En dekorator med argumenter behøver ét lag mere: en funktion der tager argumenterne og returnerer en dekorator. Det er tre niveauer af nesting, og det forvirrer næsten alle første gang de ser det. Her er en @retry-dekorator der forsøger en funktion op til max_attempts gange ved undtagelse:

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()

Kaldkæden når Python behandler @retry(max_attempts=4, delay=0.5, backoff=2.0): først kaldes retry(max_attempts=4, delay=0.5, backoff=2.0) og returnerer decorator. Derefter kaldes decorator(fetch_price_data) og returnerer wrapper. Endelig genopbindes fetch_price_data til wrapper. Så @retry(...) er fetch_price_data = retry(...)(fetch_price_data) — tre kald, to niveauer af omslutning. Når du først ser det mønster, holder dekoratorfabrikker op med at være forvirrende.

Klassebaserede dekoratorer

Du kan også implementere en dekorator som en klasse ved at definere __call__. Det er nyttigt, når dekoratoren skal opretholde tilstand mellem kald — en kaldtæller, et cache, forbindelsespuljer — fordi instansvariabler er en mere naturlig plads for den tilstand end closure-variabler.

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) fungerer præcis som en dekoratorfabrik: RateLimiter(2.0) konstruerer en instans, derefter kaldes den instans med send_sms_alert fordi den har __call__. Instansen gemmer _last_called som et attribut — ingen jonglering med closure-variabler. Se PEP 318 (det originale dekorator-forslag) for designbegrundelsen bag @-syntaksen, og PEP 614 for afslappet dekorator-grammatik der landede i Python 3.9.

Virkelige dekoratorer fra standardbiblioteket

Inden du skriver din egen, check om stdlib allerede har det du har brug for. Tre dekoratorer i functools dukker op hele tiden i Python-kode i produktion.

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 er i en anden kategori — det er en klassedekorator der automatisk genererer __init__, __repr__ og __eq__ fra dine felt-annotationer. Det skærer ned på en betydelig mængde boilerplate for dataholdende klasser:

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, ...)

Dekoratoren @property omdanner en metode til en attributlignende accessor — kaldere læser user.display_name i stedet for user.get_display_name(). Kombinér den med @property.setter for at validere ved skrivning:

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

En @require_auth-dekorator til route-handlere

Autentificeringstjek i webframeworks er et lærebogs-anvendelsestilfælde for dekoratorer. I stedet for at duplikere "er brugeren logget ind?"-tjekket i starten af hvert route-handler, skriver du det én gang som en dekorator og anvender det der, hvor det er nødvendigt. Her er mønstret, skrevet til at fungere med Flask men overførbart til ethvert 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())

Bemærk rækkefølgen: @app.route går først (yderst), @require_auth går på andenpladsen. Det betyder noget — se næste afsnit. Mønstret udvider sig naturligt: du kan tilføje en dekoratorfabrik @require_role("admin") der tjekker g.current_user.role efter at @require_auth allerede har verificeret at brugeren eksisterer.

Stablede dekoratorer — rækkefølge betyder noget

Når du stabler flere dekoratorer, anvendes de nedefra og op (dekoratoren tættest på funktionen anvendes først), men de udføres oppefra og ned når funktionen kaldes. Det overrasker folk.

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.
Stack trace-tip: Når en dekoreret funktion kaster en undtagelse, viser traceback enhver wrapper i kaldstakken. Hvis du brugte @functools.wraps korrekt, vil navnene afspejle de originale funktioner. Hvis du ser et hav af wrapper-rammer, glemte nogen @functools.wraps. Real Python's decorator-primer har en god gennemgang af, hvordan man debugger stablede dekoratorer.

Opsummering

Den mentale model at bære med sig: @decorator er fn = decorator(fn). Alt andet — dekoratorer med argumenter, klassebaserede dekoratorer, stablede dekoratorer — er en variation af den ene substitution. Brug @functools.wraps på enhver indre wrapper, videresend altid *args, **kwargs til den omsluttede funktion og returner dens resultat. Dekoratorer med argumenter behøver tre niveauer af nesting: argumentfunktionen, dekoratoren og wrapperen. Klassebaserede dekoratorer er det rette valg, når din dekorator skal opretholde tilstand mellem kald.

Til videre læsning: Pythons glossary-indgang om dekoratorer er kort men præcis. Det originale dekorator-forslag, PEP 318, er værd at læse for kontekst om, hvorfor @-syntaksen blev valgt frem for alternativer. Hvis du bruger dekoratorer i en kodebase der også laver en masse databehandling — læsning af filer, transformering af poster — passer mønstrene her naturligt sammen med det der dækkes i Python filhåndtering. Og hvis du bruger dekoratorer til at transformere samlinger eller bygge opslagsstrukturer, dækker Python listeforståelser datatransformationssiden af det billede. Hvis dine dekorerede funktioner returnerer JSON-output og du vil inspicere det hurtigt, er JSON Formatter på dette site praktisk til det.