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.

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

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

Nå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.

Desugaring-regeln: Varje gång du ser @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 wrapper istä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_cache och liknande verktyg använder funktionens identitet för cache-nycklar — utan wraps kan du få överraskande 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 ä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:

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

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.

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

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 ä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:

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

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:

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 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:

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

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.

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 dekorerad funktion kastar ett undantag visar traceback varje wrapper i anropsstacken. Om du använde @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.