Hvis du har brukt Flask eller FastAPI i mer enn en uke, har du allerede brukt dekoratorer — @app.route, @login_required, @pytest.mark.parametrize. De føles som magi første gang, og deretter forklarer noen hva som faktisk skjer og det klikker umiddelbart. En dekorator er bare en funksjon som pakker inn en annen funksjon. @-syntaksen er rent syntaktisk sukker — @my_decorator over en funksjondefinisjon er nøyaktig ekvivalent med å skrive func = my_decorator(func) etter den. Det er hele hemmeligheten. Resten er bare mønstre bygget oppå den ene ideen. Denne artikkelen bygger den mentale modellen fra bunnen av, og går deretter gjennom mønstrene du faktisk vil bruke: @functools.wraps, dekoratorer med argumenter, klassebaserte dekoratorer, og en håndfull virkelige eksempler inkludert @lru_cache, @dataclass, og en skikkelig @retry med eksponentiell backoff.

Funksjoner er førsteklasses objekter

Før dekoratorer gir mening, må du ha kontroll på ett Python-faktum: funksjoner er objekter. Du kan tildele dem til variabler, sende dem som argumenter til andre funksjoner, returnere dem fra funksjoner, og lagre dem i lister eller dictionaries. Ingenting spesielt skjer bare fordi noe er en funksjon.

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 funksjonen prefixed_greet "lukker over" variabelen prefix fra det omsluttende omfanget — selv etter at make_prefixer har returnert, har den indre funksjonen fortsatt tilgang til prefix. Dette er en closure, og det er mekanismen som driver dekoratorer. Pythons dokumentasjon om omfangsregler forklarer dette i detalj hvis du ønsker det fullstendige bildet.

Bygge en dekorator fra bunnen av

En dekorator er en funksjon som tar en funksjon og returnerer en (vanligvis modifisert) funksjon. Det klassiske første eksempelet er en timing-dekorator — den pakker inn enhver funksjon og logger hvor lang tid den tok å kjø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

Noen ting å legge merke til: wrapperen bruker *args, **kwargs slik at den kan videresende enhver kombinasjon av argumenter til den originale funksjonen uten å kjenne dens signatur. Den fanger returverdien i result og returnerer den, slik at den innpakkede funksjonen fortsatt oppfører seg identisk fra kallerens perspektiv — den har bare en ekstra utskrift som bieffekt. Fjern timingen, og du har skjelettet til nesten enhver dekorator du noen gang vil skrive.

Desugaring-regelen: Når du ser @some_decorator over en funksjondefinisjon, erstatt det mentalt med fn = some_decorator(fn) skrevet direkte etter def-blokken. De to er nøyaktig ekvivalente. Det er ingen magi — det er et funksjonskall.

Hvorfor du må bruke @functools.wraps

I eksempelet ovenfor er det en @functools.wraps(fn)-linje på wrapperen. Dette er ikke valgfritt. Uten det mister den dekorerte funksjonen sin identitet — dens __name__, __doc__ og __qualname__-attributter erstattes alle med de fra den indre wrapper-funksjonen. Det forårsaker subtile feil i noen virkelige situasjoner:

  • Docstrings forsvinner. help(fetch_user_records) viser wrapperens tomme docstring i stedet for "Fetch all records for a given user...".
  • Stack traces lyver. Når et unntak oppstår inne i den innpakkede funksjonen, viser traceback wrapper i stedet for det virkelige funksjonsnavnet — vanskelig å debugge.
  • Introspeksjon brytes. Verktøy som pytest, Flasks rutingsystem og inspect.signature() er alle avhengige av __name__ og __wrapped__. Flasks ruter vil kaste hvis to ruter deler det samme (wrapper-) navnet.
  • functools.lru_cache og lignende verktøy bruker funksjonens identitet for cache-nøkling — uten wraps kan du få overraskende cache-kollisjoner.
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 selv en dekorator — den kopierer __module__, __name__, __qualname__, __annotations__, __doc__ og setter __wrapped__ til den originale funksjonen. Bruk den på enhver wrapper-funksjon, punktum. Det er ingen grunn til å ikke gjøre det.

Dekoratorer med argumenter

En vanlig dekorator tar en funksjon og returnerer en funksjon. En dekorator med argumenter trenger ett nivå til: en funksjon som tar argumentene og returnerer en dekorator. Det er tre nivåer av nesting, og det forvirrer nesten alle første gang de ser det. Her er en @retry-dekorator som forsøker en funksjon opp til max_attempts ganger ved unntak:

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

Kallkjeden når Python behandler @retry(max_attempts=4, delay=0.5, backoff=2.0): først kalles retry(max_attempts=4, delay=0.5, backoff=2.0) og returnerer decorator. Deretter kalles decorator(fetch_price_data) og returnerer wrapper. Til slutt gjenopbindes fetch_price_data til wrapper. Så @retry(...) er fetch_price_data = retry(...)(fetch_price_data) — tre kall, to nivåer av innpakning. Når du en gang ser det mønsteret, slutter dekoratorfabrikker å være forvirrende.

Klassebaserte dekoratorer

Du kan også implementere en dekorator som en klasse ved å definere __call__. Dette er nyttig når dekoratoren trenger å opprettholde tilstand mellom kall — en kallteller, et cache, tilkoblingspooler — fordi instansvariabler er et mer naturlig sted for den tilstanden enn 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 akkurat som en dekoratorfabrikk: RateLimiter(2.0) konstruerer en instans, deretter kalles den instansen med send_sms_alert fordi den har __call__. Instansen lagrer _last_called som et attributt — ingen jonglering med closure-variabler. Se PEP 318 (det originale dekorator-forslaget) for designbegrunnelsen bak @-syntaksen, og PEP 614 for avslappet dekorator-grammatikk som landet i Python 3.9.

Virkelige dekoratorer fra standardbiblioteket

Før du skriver din egen, sjekk om stdlib allerede har det du trenger. Tre dekoratorer i functools dukker opp hele tiden i Python-kode i produksjon.

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 annen kategori — det er en klassedekorator som automatisk genererer __init__, __repr__ og __eq__ fra felt-annotasjonene dine. Det kutter ned på en betydelig mengde 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 gjør om en metode til en attributt-lignende accessor — kallere leser user.display_name i stedet for user.get_display_name(). Kombiner den med @property.setter for å validere ved skriving:

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 for route-handlere

Autentiseringssjekker i web-rammeverk er et læreboks-brukstilfelle for dekoratorer. I stedet for å duplisere "er brukeren logget inn?"-sjekken øverst i hvert route-handler, skriver du den én gang som en dekorator og anvender den der det trengs. Her er mønsteret, skrevet for å fungere med Flask, men overførbart til ethvert rammeverk:

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

Merk rekkefølgen: @app.route går først (ytterst), @require_auth går på andreplass. Det betyr noe — se neste seksjon. Mønsteret utvider seg naturlig: du kan legge til en dekoratorfabrikk @require_role("admin") som sjekker g.current_user.role etter at @require_auth allerede har verifisert at brukeren eksisterer.

Stablede dekoratorer — rekkefølge betyr noe

Når du stabler flere dekoratorer, brukes de nedenfra og opp (dekoratoren nærmest funksjonen brukes først), men de utføres ovenfra og ned når funksjonen kalles. Dette 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-tips: Når en dekorert funksjon kaster et unntak, viser traceback enhver wrapper i kallstakken. Hvis du brukte @functools.wraps korrekt, vil navnene gjenspeile de originale funksjonene. Hvis du ser et hav av wrapper-rammer, glemte noen @functools.wraps. Real Python's dekorator-primer har en god gjennomgang av hvordan man debugger stablede dekoratorer.

Oppsummering

Den mentale modellen å ta med seg: @decorator er fn = decorator(fn). Alt annet — dekoratorer med argumenter, klassebaserte dekoratorer, stablede dekoratorer — er en variasjon av den ene substitusjon. Bruk @functools.wraps på enhver indre wrapper, videresend alltid *args, **kwargs til den innpakkede funksjonen og returner resultatet. Dekoratorer med argumenter trenger tre nivåer av nesting: argumentfunksjonen, dekoratoren og wrapperen. Klassebaserte dekoratorer er det rette valget når dekoratoren din trenger å opprettholde tilstand mellom kall.

For videre lesning: Pythons ordlisteinngang om dekoratorer er kort men presis. Det originale dekorator-forslaget, PEP 318, er verdt å lese for kontekst om hvorfor @-syntaksen ble valgt fremfor alternativer. Hvis du bruker dekoratorer i en kodebase som også gjør mye databehandling — lesing av filer, transformering av poster — passer mønstrene her naturlig sammen med det som dekkes i Python filhåndtering. Og hvis du bruker dekoratorer til å transformere samlinger eller bygge oppslagsstrukturer, dekker Python listeforståelser datatransformasjonssiden av det bildet. Hvis de dekorerte funksjonene dine returnerer JSON-utdata og du vil inspisere det raskt, er JSON Formatter på dette nettstedet praktisk for det.