Programátorské dekorace: proč si funkce zaslouží ozdobu
Dekorátory v Pythonu rozšiřují funkce bez změny jejich kódu. V článku zjistíte, co znamená, že funkce je objekt, jak fungují třídy a instance, a krok za krokem si ukážeme, jak napsat vlastní dekorátor a prakticky ho využít pro logování, měření času či kontrolu přístupu.
Pokud se učíte programovat v Pythonu, dříve nebo později na dekorátory narazíte. A většinu začátečníků buď fascinují, nebo rozčilují. Vyberte si svůj případ.
Než si ale vysvětlíme, co dekorátor vlastně je, podívejme se nejprve na základní princip — totiž na to, že funkce je v Pythonu objekt.
Objekt, objekt… slyšíte to pořád dokola. Ale co to vlastně znamená?
Objekt je kus dat uložený v paměti, který má:
- identitu (tedy unikátní adresu v paměti),
- typ (například
int,str,listatd.), - hodnotu (například číslo 42).
x = 42
print(id(x)) # identita (unikátní číslo v paměti)
print(type(x)) # typ: <class 'int'>
print(x) # hodnota: 42
Každý objekt je zároveň instancí nějaké třídy.
To možná zatím moc nepomůže – máme tu totiž další pojmy: třída a instance.
Třídu si můžete představit jako plán nebo šablonu, podle které se vytvářejí konkrétní objekty.
Například při stavbě domu potřebujete projekt. Tento projekt je „třída“ a konkrétní postavený dům je „instance“.
Podobně i auto má nejprve technickou dokumentaci a výkresy (to je třída). Každé vyrobené auto s vlastním VIN číslem a SPZ je pak instance třídy Auto.
A úplně stejně i my, lidé, jsme instance třídy — a tou třídou je náš genetický kód.
Každý plán, šablona neboli třída má své vlastnosti – tedy informace, které objekt obsahuje – a také schopnosti, tedy akce, které může provádět.
V Pythonu těmto dvěma typům vlastností říkáme:
- atributy (vlastnosti, data),
- metody (funkce, které objekt „umí“).
Například auto má tyto atributy:
barvu, počet dveří, značku, typ, umístění řízení (vpravo nebo vlevo), počet převodů, maximální rychlost nebo zrychlení.
A může vykonávat tyto akce: jezdit, zrychlovat, zatáčet, brzdit, stírat, blikat, svítit, topit atd.
V Pythonu bychom takovou třídu mohli zapsat například takto:
class Auto:
def __init__(
self,
znacka: str,
typ: str,
barva: str,
pocet_dveri: int,
rizeni_vpravo: bool,
pocet_prevodu: int,
max_rychlost: float,
zrychleni_0_100: float
):
self.znacka = znacka
self.typ = typ
self.barva = barva
self.pocet_dveri = pocet_dveri
self.rizeni_vpravo = rizeni_vpravo
self.pocet_prevodu = pocet_prevodu
self.max_rychlost = max_rychlost
self.zrychleni_0_100 = zrychleni_0_100
self.aktualni_rychlost = 0
self.motor_bezi = False
self.svetla_zapnuta = False
# --- Akce auta ---
def nastartuj(self):
if not self.motor_bezi:
self.motor_bezi = True
print(f"{self.znacka} {self.typ} nastartovalo motor.")
else:
print("Motor už běží.")
def jezdi(self, rychlost: float):
if not self.motor_bezi:
print("Nejdřív musíš nastartovat motor!")
return
self.aktualni_rychlost = min(rychlost, self.max_rychlost)
print(f"AUTO jede rychlostí {self.aktualni_rychlost} km/h.")
def zrychli(self, o_kolik: float):
if not self.motor_bezi:
print("Motor neběží, nejde zrychlovat.")
return
self.aktualni_rychlost = min(
self.aktualni_rychlost + o_kolik, self.max_rychlost
)
print(f"Zrychluji na {self.aktualni_rychlost} km/h.")
def brzd(self, o_kolik: float):
self.aktualni_rychlost = max(self.aktualni_rychlost - o_kolik, 0)
print(f"Brzdím, aktuální rychlost: {self.aktualni_rychlost} km/h.")
def zatoc(self, smer: str):
if smer.lower() in ("vlevo", "vpravo"):
print(f"Zatáčím {smer}.")
else:
print("Směr musí být 'vlevo' nebo 'vpravo'.")
def svetla(self, zapnout: bool):
self.svetla_zapnuta = zapnout
stav = "zapnuta" if zapnout else "vypnuta"
print(f"Světla jsou {stav}.")
def top(self, zapnout: bool):
stav = "zapnuto" if zapnout else "vypnuto"
print(f"Topení je {stav}.")
def __str__(self):
return f"{self.znacka} {self.typ} ({self.barva}, {self.pocet_dveri} dveře)"
Z této třídy si pak můžeme vytvořit instanci – tedy konkrétní auto:
moje_auto = Auto(
znacka="Škoda",
typ="Octavia",
barva="modrá",
pocet_dveri=5,
rizeni_vpravo=False,
pocet_prevodu=6,
max_rychlost=220,
zrychleni_0_100=8.5
)
print(moje_auto)
moje_auto.nastartuj()
moje_auto.jezdi(80)
moje_auto.zrychli(30)
moje_auto.zatoc("vpravo")
moje_auto.brzd(50)
moje_auto.svetla(True)
moje_auto.top(True)
Z tohoto příkladu je patrné, že atributy (vlastnosti objektu) zapisujeme pomocí self.atribut.
Klíčové slovo self se používá k označení samotné instance, tedy „sebe“.
Metody jsou naopak funkce definované uvnitř třídy – poznáte je podle def.
Jediný rozdíl oproti běžným funkcím je, že jejich prvním parametrem je právě self, který Python předává automaticky při volání metody.
Funkce jako objekt
Teď už víme, že objekty mají atributy (data) a metody (schopnosti).
A víme také, že objekty vznikají jako instance tříd.
Ale teď přichází moment překvapení: v Pythonu je funkce také objekt.
To znamená, že funkci můžete:
- uložit do proměnné,
- předat jako argument jiné funkci,
- vrátit z funkce jako návratovou hodnotu,
- nebo jí dokonce přidat atributy.
Podívejme se na jednoduchý příklad:
def pozdrav():
print("Ahoj!")
# Funkci lze uložit do proměnné
f = pozdrav
# A zavolat přes tuto proměnnou
f()
Výstup bude:
Ahoj!
Tady vidíme, že f i pozdrav jsou ve skutečnosti reference na stejný objekt funkce v paměti.
Funkce má tedy stejný status jako jakýkoli jiný objekt (např. číslo, řetězec nebo seznam).
Tohle je klíčové pro pochopení dekorátorů.
Dekorátor je totiž funkce, která přijímá jinou funkci jako argument a vrací novou funkci, která původní rozšiřuje nebo upravuje její chování.
Zkusme jednoduchý příklad bez speciální syntaktické zkratky:
def pozdrav():
print("Ahoj!")
def dekorator(funkce):
def obalena_funkce(*args, **kwargs):
print("Před voláním funkce...")
funkce(*args, **kwargs)
print("Po volání funkce...")
return obalena_funkce
# aplikujeme dekorátor ručně
pozdrav = dekorator(pozdrav)
pozdrav()
Výstup:
Před voláním funkce...
Ahoj!
Po volání funkce...
Tady dekorator() přijme funkci pozdrav, „zabalí“ ji do jiné funkce a vrátí ji zpět.
A přesně to dělají dekorátory v Pythonu — jen k tomu mají pohodlnější zápis pomocí znaku @:
@dekorator
def pozdrav():
print("Ahoj!")
Tento zápis je naprosto ekvivalentní k ručnímu přiřazení:
pozdrav = dekorator(pozdrav)
Ještě se podrobněji podíváme, co dělá obalena_funkce a proč má args a **kwargs.
Když vytváříme dekorátor, chceme, aby fungoval pro libovolnou funkci, bez ohledu na to, kolik má argumentů.
A právě k tomu slouží tzv. obalovací funkce (angl. wrapper function), kterou v příkladech pojmenovávám obalena_funkce.
Co obalena_funkce dělá:
- Převezme volání původní funkce, tedy všechna data (argumenty), která by jinak dostala.
- Může před voláním něco provést (např. logování, měření času, kontrolu oprávnění…).
- Zavolá původní funkci.
- Může po volání opět něco udělat (např. zapsat výsledek, změřit čas, vrátit výsledek dál).
- Vrátí výsledek původní funkce, aby se dekorátor choval z pohledu uživatele úplně stejně jako původní funkce.
Proč *args a **kwargs?
V Pythonu může mít každá funkce jiný počet argumentů:
def bez_arg():
...
def s_jednim(a):
...
def s_dvema(a, b):
...
def univerzalni(*args, **kwargs):
...
Kdybychom v dekorátoru napsali:
def obalena_funkce(a, b):
funkce(a, b)
tak by náš dekorátor fungoval jen pro funkce se dvěma argumenty.
Jakmile bychom ho použili na funkci s jiným počtem argumentů, Python by vyhodil chybu.
Proto se používají parametry *args a **kwargs, které dokážou zachytit libovolný počet pozičních i pojmenovaných argumentů:
*args= n-tice všech pozičních argumentů (např.f(1, 2, 3)→args = (1, 2, 3)),**kwargs= slovník všech pojmenovaných argumentů (např.f(x=10, y=20)→kwargs = {'x': 10, 'y': 20}).
Tím zajistíme, že dekorátor funguje univerzálně:
def dekorator(funkce):
def obalena_funkce(*args, **kwargs):
print("Před voláním...")
vysledek = funkce(*args, **kwargs)
print("Po volání...")
return vysledek
return obalena_funkce
Můžeme ho pak použít na jakoukoli funkci – s žádným, jedním nebo deseti argumenty – a bude stále fungovat.
Praktické využití dekorátorů
Teď, když už víme, že dekorátor je funkce, která „obalí“ jinou funkci a může ji tím rozšířit o nové chování, ukažme si pár typických příkladů z praxe.
1. Měření doby běhu funkce
Chceme zjistit, jak dlouho trvá vykonání nějaké funkce.
Bez dekorátoru bychom do každé funkce museli ručně vkládat kód na měření času.
S dekorátorem to zvládneme elegantně a znovupoužitelně:
import time
def mer_cas(funkce):
def obalena_funkce(*args, **kwargs):
start = time.time()
vysledek = funkce(*args, **kwargs)
konec = time.time()
print(f"Funkce {funkce.__name__} trvala {konec - start:.4f} s")
return vysledek
return obalena_funkce
@mer_cas
def secti(a, b):
time.sleep(1) # simulace pomalejší operace
return a + b
print(secti(10, 20))
Výstup:
Funkce secti trvala 1.0003 s
30
Dekorátor @mer_cas elegantně přidal časové měření, aniž bychom museli měnit samotnou funkci secti.
Pro jistotu sem vložím jak by vypadal stejný příklad bez použití magického @.
import time
def mer_cas(funkce):
def obalena_funkce(*args, **kwargs):
start = time.time()
vysledek = funkce(*args, **kwargs)
konec = time.time()
print(f"Funkce {funkce.__name__} trvala {konec - start:.4f} s")
return vysledek
return obalena_funkce
def secti(a, b):
time.sleep(1)
return a + b
# Aplikujeme dekorátor ručně
secti = mer_cas(secti)
print(secti(10, 20))
2. Logování volání funkcí
Dekorátory se často používají i pro logování – například chceme zjistit, kdy a s jakými argumenty byla funkce volána:
def loguj_volani(funkce):
def obalena(*args, **kwargs):
print(f"Volám funkci {funkce.__name__} s argumenty {args}, {kwargs}")
return funkce(*args, **kwargs)
return obalena
@loguj_volani
def pozdrav(jmeno):
print(f"Ahoj, {jmeno}!")
pozdrav("Daniel")
Výstup:
Volám funkci pozdrav s argumenty ('Daniel',), {}
Ahoj, Daniel!
3. Kontrola přístupu
Dalším častým využitím je kontrola oprávnění – například chceme spouštět funkci jen tehdy, když má uživatel potřebnou roli:
def vyzaduje_admina(funkce):
def obalena(user_role):
if user_role != "admin":
print("Přístup odepřen.")
return
return funkce(user_role)
return obalena
@vyzaduje_admina
def smaz_data(user_role):
print("Data byla smazána!")
smaz_data("student") # Přístup odepřen
smaz_data("admin") # Data byla smazána!
Dekorátory jsou tedy čistý a znovupoužitelný způsob, jak měnit chování funkcí bez zásahu do jejich vnitřního kódu.
V Pythonu se používají velmi často — například u tříd (@dataclass), statických metod (@staticmethod), nebo v knihovnách jako FastAPI, Flask, pytest, TensorFlow a mnoha dalších.