Kas tie decoratoriai ir kodėl jie tokie šaunūs
Kai pirmą kartą susiduri su Python decoratoriais, gali atrodyti, kad tai kažkoks magiškas @ simbolis, kuris tiesiog kabinėjamas virš funkcijų. Tiesą sakant, decoratoriai yra vienas iš tų Python elementų, kurie iš pradžių atrodo kaip nereikalinga komplikacija, bet vėliau supranti – be jų gyvenimas būtų daug sunkesnis.
Paprasčiausiai tariant, decoratorius yra funkcija, kuri priima kitą funkciją ir išplečia jos funkcionalumą nekeisdama pačios funkcijos kodo. Skamba sudėtingai? Įsivaizduok, kad turi namą ir nori pridėti jam terasą. Tu neperstatinei viso namo – tiesiog pridedam papildomą elementą, kuris padaro namą funkcionalesnį. Tas pats ir su decoratoriais.
Štai paprasčiausias pavyzdys:
„`python
def mano_decoratorius(funkcija):
def wrapper():
print(„Kažkas vyksta prieš funkciją”)
funkcija()
print(„Kažkas vyksta po funkcijos”)
return wrapper
@mano_decoratorius
def pasakyk_labas():
print(„Labas!”)
pasakyk_labas()
„`
Šis kodas išspausdins tris eilutes: pranešimą prieš, patį „Labas!” ir pranešimą po. Decoratorius @ sintaksė yra tiesiog gražesnis būdas parašyti `pasakyk_labas = mano_decoratorius(pasakyk_labas)`.
Kaip iš tikrųjų veikia šis mechanizmas
Norint suprasti decoratorius, reikia pirmiausia suvokti, kad Python’e funkcijos yra pirmosios klasės objektai. Tai reiškia, kad galima jas perduoti kaip argumentus, grąžinti iš kitų funkcijų ir priskirti kintamiesiems. Jei esi programavęs JavaScript, tai tau turėtų skambėti pažįstamai.
Decoratorius veikia trimis pagrindiniais žingsniais:
1. Priima funkciją kaip argumentą
2. Apibrėžia vidinę wrapper funkciją, kuri papildo originalios funkcijos elgesį
3. Grąžina tą wrapper funkciją
Svarbiausia čia suprasti, kad originalios funkcijos mes nekeičiame. Mes tiesiog sukuriame naują funkciją, kuri „įvynioja” seną ir prideda papildomų galimybių. Todėl ir vadinasi wrapper – įvyniotojas.
Argumentai ir parametrai: kai reikalai komplikuojasi
Gerai, bet kas nutinka, kai mūsų funkcija priima argumentus? Ankstesnis pavyzdys veikia tik su funkcijomis be parametrų. Realybėje dauguma funkcijų priima bent vieną argumentą, todėl mūsų decoratorius turi būti lankstesnis.
„`python
def mano_decoratorius(funkcija):
def wrapper(*args, **kwargs):
print(f”Kviečiama funkcija su argumentais: {args}, {kwargs}”)
rezultatas = funkcija(*args, **kwargs)
print(f”Funkcija grąžino: {rezultatas}”)
return rezultatas
return wrapper
@mano_decoratorius
def sudek(a, b):
return a + b
print(sudek(5, 3))
„`
Čia naudojame `*args` ir `**kwargs`, kurie leidžia mūsų wrapper funkcijai priimti bet kokį skaičių pozicijų ir įvardintų argumentų. Tai universalus sprendimas, kuris veiks su bet kokia funkcija.
Dar vienas svarbus dalykas – nepamirškite grąžinti funkcijos rezultato. Jei to nepadarysite, jūsų dekoruota funkcija visada grąžins `None`, ir tai gali sukelti keistų klaidų, kurias sunku aptikti.
Praktiniai panaudojimo atvejai
Gerai, teorija suprantama, bet kam to reikia realiame gyvenime? Štai keletas situacijų, kur decoratoriai išgelbsti:
**Logging’as ir debugging’as**
Vienas populiariausių decoratorių panaudojimų – funkcijų iškvietimų registravimas. Vietoj to, kad kiekvienoje funkcijoje rašytum `print()` ar `logger.info()`, galima sukurti vieną decoratorių:
„`python
import functools
import time
def log_execution(funkcija):
@functools.wraps(funkcija)
def wrapper(*args, **kwargs):
print(f”Vykdoma: {funkcija.__name__}”)
start = time.time()
rezultatas = funkcija(*args, **kwargs)
end = time.time()
print(f”Užtruko: {end – start:.4f} sekundžių”)
return rezultatas
return wrapper
„`
Pastebėjote `@functools.wraps`? Tai irgi decoratorius! Jis išsaugo originalios funkcijos metaduomenis (pavadinimą, docstring ir pan.). Be jo, jūsų dekoruota funkcija prarastų savo identitetą ir visur atrodytų kaip „wrapper”.
**Autentifikacija ir autorizacija**
Web aplikacijose dažnai reikia patikrinti, ar vartotojas prisijungęs prieš leidžiant jam pasiekti tam tikras funkcijas:
„`python
def reikalauja_prisijungimo(funkcija):
@functools.wraps(funkcija)
def wrapper(*args, **kwargs):
if not current_user.is_authenticated:
raise PermissionError(„Turite būti prisijungę”)
return funkcija(*args, **kwargs)
return wrapper
@reikalauja_prisijungimo
def perziureti_profili():
return „Jūsų profilio informacija”
„`
Flask framework’e tokius decoratorius matote nuolat – `@app.route()`, `@login_required` ir panašiai.
**Caching’as**
Jei turite funkciją, kuri atlieka sunkius skaičiavimus, galite išsaugoti rezultatus:
„`python
def cache_rezultatus(funkcija):
cache = {}
@functools.wraps(funkcija)
def wrapper(*args):
if args not in cache:
cache[args] = funkcija(*args)
return cache[args]
return wrapper
@cache_rezultatus
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
```
Beje, Python'e jau yra įtaisytas `@functools.lru_cache`, kuris daro būtent tai, tik daug geriau.
Decoratoriai su parametrais: meta lygmuo
Dabar tampa įdomu. Ką daryti, jei norite, kad pats decoratorius priimtų parametrus? Pavyzdžiui, norėtumėte nurodyti, kiek kartų pakartoti funkcijos vykdymą:
„`python
def kartok(kartai):
def decoratorius(funkcija):
@functools.wraps(funkcija)
def wrapper(*args, **kwargs):
for _ in range(kartai):
rezultatas = funkcija(*args, **kwargs)
return rezultatas
return wrapper
return decoratorius
@kartok(kartai=3)
def pasakyk_oi():
print(„Oi!”)
„`
Čia jau turime tris funkcijų lygius! `kartok` yra funkcija, kuri grąžina decoratorių, kuris grąžina wrapper. Galva svaigsta? Normalus reiškinys. Tiesiog įsidėmėkite šabloną: kai reikia parametrų, pridedame dar vieną funkcijos lygį.
Praktiškesnis pavyzdys – retry mechanizmas API užklausoms:
„`python
import time
def retry(bandymai=3, delsa=1):
def decoratorius(funkcija):
@functools.wraps(funkcija)
def wrapper(*args, **kwargs):
for bandymas in range(bandymai):
try:
return funkcija(*args, **kwargs)
except Exception as e:
if bandymas == bandymai – 1:
raise
print(f”Klaida: {e}. Bandoma dar kartą po {delsa}s…”)
time.sleep(delsa)
return wrapper
return decoratorius
@retry(bandymai=5, delsa=2)
def nestabili_api_uzklausa():
# Kodas, kuris gali kartais nepavykti
pass
„`
Klasių decoratoriai ir metodai
Decoratoriai veikia ne tik su paprastomis funkcijomis. Galite dekoruoti klasių metodus, statines funkcijas, net pačias klases.
Kai dekoruojate klasės metodą, reikia atsiminti, kad pirmasis argumentas bus `self`:
„`python
class ManoKlase:
@log_execution
def metodas(self, x):
return x * 2
„`
Tai veikia be problemų su mūsų ankstesniu `log_execution` decoratoriumi, nes naudojame `*args, **kwargs`.
Python turi ir įtaisytų decoratorių klasėms:
– `@property` – paverčia metodą į property, kurį galima pasiekti kaip atributą
– `@staticmethod` – metodas, kuris nepriklauso nuo klasės instancijos
– `@classmethod` – metodas, kuris gauna klasę kaip pirmąjį argumentą
„`python
class Produktas:
def __init__(self, kaina, pvm_procentas=21):
self._kaina = kaina
self.pvm_procentas = pvm_procentas
@property
def kaina_su_pvm(self):
return self._kaina * (1 + self.pvm_procentas / 100)
@staticmethod
def ar_galiojantis_kodas(kodas):
return len(kodas) == 13
@classmethod
def is_euro(cls, kaina_eurais):
return cls(kaina_eurais)
„`
Dažniausios klaidos ir kaip jų išvengti
Per metus matęs šimtus decoratorių implementacijų, galiu pasakyti, kad kai kurios klaidos pasikartoja nuolat.
**Pamirštas @functools.wraps**
Jau minėjau, bet pakartosiu – visada naudokite `@functools.wraps(funkcija)` savo wrapper funkcijoje. Kitaip prarandate funkcijos pavadinimą, docstring, ir debugging’as tampa košmaru.
**Grąžinimo reikšmės ignoravimas**
„`python
def blogas_decoratorius(funkcija):
def wrapper(*args, **kwargs):
print(„Prieš”)
funkcija(*args, **kwargs) # Kur rezultatas?!
print(„Po”)
return wrapper
„`
Šis decoratorius prarys funkcijos grąžinamą reikšmę. Visada įsitikinkite, kad grąžinate `funkcija(*args, **kwargs)`, nebent tikrai norite pakeisti grąžinamą reikšmę.
**Perdėtas komplikavimas**
Ne visur reikia decoratorių. Jei jūsų decoratorius naudojamas tik vienoje vietoje, galbūt paprasčiau būtų tiesiog parašyti papildomą kodą funkcijoje. Decoratoriai puikūs, kai tas pats funkcionalumas reikalingas daugelyje vietų.
**Eilės tvarka**
Kai naudojate kelis decoratorius, jie taikomi iš apačios į viršų:
„`python
@decoratorius_a
@decoratorius_b
@decoratorius_c
def funkcija():
pass
„`
Tai ekvivalentu `decoratorius_a(decoratorius_b(decoratorius_c(funkcija)))`. Eilės tvarka gali būti svarbi, ypač jei decoratoriai keičia argumentus ar grąžinamas reikšmes.
Pažangesnės technikos ir triukai
Kai jau jaučiatės patogiai su baziniais decoratoriais, galite išbandyti pažangesnius dalykus.
**Klasės kaip decoratoriai**
Vietoj funkcijos, decoratorius gali būti klasė su `__call__` metodu:
„`python
class SkaiciuokIskvietus:
def __init__(self, funkcija):
self.funkcija = funkcija
self.iskvietu_skaicius = 0
def __call__(self, *args, **kwargs):
self.iskvietu_skaicius += 1
print(f”Iškvietimas #{self.iskvietu_skaicius}”)
return self.funkcija(*args, **kwargs)
@SkaiciuokIskvietus
def sveikink():
print(„Sveikas!”)
„`
Klasių decoratoriai naudingi, kai reikia išlaikyti būseną tarp iškvietimų.
**Decoratorių grandinės**
Galite sukurti decoratorių, kuris pats grąžina decoratorių su parametrais:
„`python
def smart_cache(max_size=128, ttl=None):
def decoratorius(funkcija):
cache = {}
timestamps = {}
@functools.wraps(funkcija)
def wrapper(*args, **kwargs):
key = (args, tuple(kwargs.items()))
if ttl and key in timestamps:
if time.time() – timestamps[key] > ttl:
del cache[key]
del timestamps[key]
if key not in cache:
if len(cache) >= max_size:
oldest = min(timestamps.items(), key=lambda x: x[1])[0]
del cache[oldest]
del timestamps[oldest]
cache[key] = funkcija(*args, **kwargs)
timestamps[key] = time.time()
return cache[key]
return wrapper
return decoratorius
„`
Kai decoratoriai tampa jūsų superšaunu įrankiu
Decoratoriai – tai ne tik Python sintaksinis cukrus. Tai galinga abstrakcijos priemonė, leidžianti atskirti skirtingus kodo aspektus. Logging’as, autentifikacija, caching’as, validacija – visa tai gali būti elegantiškai iškeltas iš pagrindinės verslo logikos.
Pradedant naudoti decoratorius, rekomenduoju pradėti nuo paprastų dalykų – logging’o ar laiko matavimo. Kai įgausite pasitikėjimo, galėsite kurti sudėtingesnius sprendimus. Svarbu nepersistengti – decoratorius turėtų supaprastinti kodą, o ne jį komplikuoti.
Jei dirbi su Django ar Flask, jau naudoji decoratorius net to nežinodamas. Pažvelk į savo kodą – galbūt pastebėsi pasikartojančių šablonų, kuriuos būtų galima iškelti į decoratorių? Galbūt turite dešimt funkcijų, kuriose pirmiausia tikriname vartotojo teises? Ar penkias vietas, kur matuojate funkcijos vykdymo laiką? Tai idealūs kandidatai decoratoriams.
Paskutinis patarimas – kai kuriate decoratorių, pirmiausia parašykite jį be parametrų. Kai veikia, tada galvojate, ar reikia parametrų. Taip išvengsite nereikalingo komplikavimo ir lengviau debug’insite problemas. O ir nepamirškite `@functools.wraps` – tai išgelbės jus nuo daugelio keblumų ateityje.
