Decorators katika Python: ni nini, zinavyofanya kazi na jinsi ya kuandika yako
Decorator katika Python ni kazi inayopokea kazi nyingine na kurudisha mpya, kwa kawaida ni kifungashio kinachoongeza tabia kabla au baada ya wito: kurekodi kumbukumbu, kupima muda, kuhifadhi matokeo kwenye cache, kukagua ruhusa. Maandishi @decorator juu ya def ni njia fupi ya func = decorator(func); ili kifungashio kibebe jina na docstring ya kazi asili, hutumika functools.wraps.
Decorator katika Python ni kazi inayopokea kazi nyingine na kurudisha kazi mpya, kwa kawaida kifungashio (wrapper) kinachozunguka ile ya awali. Kwa njia hii tabia huongezwa kwa kazi — kurekodi kumbukumbu, kupima muda, cache, majaribio ya kurudia — bila kubadilisha msimbo wake. Decorators huandikwa kwa mstari @jina juu ya tangazo la kazi, na zinapatikana kila mahali: katika Flask na FastAPI, pytest, na maktaba ya kawaida.
Decorator kwa lugha rahisi
Fikiria zawadi na karatasi ya kuifungia. Zawadi (kazi) inabaki ile ile, lakini nje kuna safu inayotenda kwanza. Decorator ni kifungashio kama hicho: wakati wa wito, kwanza msimbo wa kifungashio unatekelezwa, kisha unaita kazi ya awali na unaweza kufanya jambo juu ya matokeo.
Hili linawezekana kwa sababu katika Python kazi ni vitu vya kawaida. Zinaweza kupitishwa kwa kazi nyingine, kurudishwa kutoka kwa kazi, na kutangazwa ndani ya kazi. Decorators zimejengwa juu ya sifa hizo tatu.
Jinsi decorator inavyofanya kazi: @ ni sukari ya sintaksia
Chukua decorator rahisi kabisa, inayochapisha ujumbe kabla na baada ya wito:
def log_calls(func):
def wrapper():
print(f"Ninaita {func.__name__}")
result = func()
print(f"{func.__name__} imekamilika")
return result
return wrapper
@log_calls
def hello():
print("Habari!")
hello()
Ninaita hello
Habari!
hello imekamilika
Maandishi @log_calls juu ya def ni sawa kabisa na mstari unaokuja baada ya tangazo:
def hello():
print("Habari!")
hello = log_calls(hello)
Sasa jina hello linaelekeza kwenye wrapper, na kazi asili inaishi ndani ya closure — wrapper inakumbuka kigezo func kutoka kwa kazi ya nje. Jambo muhimu: decorator inatumika mara moja, wakati def inatekelezwa, yaani wakati moduli inapoingizwa, na kifungashio kinafanya kazi kila mara kazi inapoitwa.
Closures na decorators
Closure ni kazi iliyoingizwa ambayo inakumbuka vigezo vya kazi ya nje hata baada ya hiyo kumaliza. Bila closures decorators zisingekuwepo: ndivyo wrapper inavyopata ufikiaji wa func. Ndani ya closure pia unaweza kuhifadhi hali, kwa mfano kaunta ya miito:
def count_calls(func):
calls = 0
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
print(f"{func.__name__}: wito Na. {calls}")
return func(*args, **kwargs)
return wrapper
Jinsi ya kuandika decorator katika Python: kiolezo cha jumla
Mfano wa kwanza una kasoro mbili: kifungashio hakipokei hoja, na kinafuta jina la kazi asili. Kiolezo kinachofanya kazi, ambacho ni vyema kuanza nacho kila decorator, ni hiki:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# msimbo kabla ya wito
result = func(*args, **kwargs)
# msimbo baada ya wito
return result
return wrapper
Kanuni tatu za kiolezo:
*args, **kwargs— kifungashio kinapokea hoja zozote na kuzipitisha mbele bila mabadiliko.return result— usisahau kurudisha matokeo, vinginevyo kazi iliyopambwa itatoaNonekila mara.@functools.wraps(func)— kuhamisha metadata ya kazi asili kwenda kwenye kifungashio.
Mfano wa decorator: kupima muda wa utekelezaji:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} ilichukua sekunde {elapsed:.3f}")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum ilichukua sekunde 0.142
Kwa nini functools.wraps inahitajika
Bila wraps, kazi iliyopambwa inapoteza jina lake, docstring na sifa nyingine — nje inaonekana wrapper:
def plain(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@plain
def add(a, b):
"""Inaongeza namba mbili."""
return a + b
print(add.__name__, add.__doc__) # wrapper None
Kwa @functools.wraps(func) kifungashio kinapata __name__, __doc__, __module__, __qualname__, __annotations__ za kazi asili, na sifa __wrapped__ inashikilia rejeleo la asili. Hili ni muhimu kwa utatuzi na traceback, kwa help(), kwa mifumo inayosajili kazi kwa jina, na kwa inspect.signature inayoonyesha sahihi halisi.
Decorator yenye vigezo (yenye hoja)
Wakati mwingine decorator inahitaji mipangilio: @retry(times=3), @app.route("/"). Kwa hili kunaongezwa kiwango kingine cha kuingizwa. Kazi ya nje inapokea vigezo na kurudisha decorator halisi — muundo huu huitwa kiwanda cha decorators (decorator factory).
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def ping():
print("ping")
ping() # ping, ping, ping — mistari matatu
Mpangilio wa wito: repeat(times=3) inarudisha decorator, ambayo inatumika kwa ping na kurudisha wrapper. Kwa maneno mengine, @repeat(times=3) ni ping = repeat(times=3)(ping).
Mfano wa vitendo — kurudia pale kosa linapotokea:
import functools
import time
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
...
Decorators kadhaa kwenye kazi moja
Decorators zinaweza kuwekwa moja juu ya nyingine. Hutumika kutoka chini kwenda juu: ile iliyo karibu zaidi na def ndiyo ya kwanza.
@timer
@log_calls
def report():
...
# sawa na hiyo:
report = timer(log_calls(report))
Wakati wa wito, safu ya nje (timer) inatekelezwa kwanza, kwa hiyo itapima pia muda wa log_calls. Mpangilio una umuhimu: kwa mfano, ikiwa decorator moja inakagua ruhusa na nyingine inahifadhi matokeo kwenye cache.
Decorators za kawaida na zilizojengwa ndani ya Python
Decorators nyingi muhimu tayari zipo katika lugha na maktaba ya kawaida:
| Decorator | Inafanya nini |
|---|---|
@property | inageuza mbinu kuwa sifa ya kusoma tu; kwa @x.setter inakuwa sifa inayodhibitiwa |
@staticmethod | mbinu ya darasa bila self, kazi ya kawaida ndani ya nafasi ya majina ya darasa |
@classmethod | mbinu inapokea darasa cls badala ya mfano — hivyo huandikwa wajenzi mbadala |
@functools.cache, @functools.lru_cache | huhifadhi matokeo kwa hoja |
@functools.wraps | hunakili metadata ya kazi kwenda kwenye kifungashio |
@dataclasses.dataclass | hutengeneza __init__, __repr__, __eq__ kwa darasa |
@contextlib.contextmanager | hugeuza jenereta kuwa meneja wa muktadha kwa with |
Cache: functools.cache na lru_cache
Decorator ya cache inakumbuka matokeo kwa kila seti ya hoja, na wito unaorudiwa unarudisha matokeo bila kuhesabu tena. Mfano wa kawaida ni namba za Fibonacci za kujirudia, ambazo bila cache zinahesabiwa kwa muda wa kiekponenti:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
functools.cache (ilionekana Python 3.9) huhifadhi matokeo yote bila kikomo. @functools.lru_cache(maxsize=128) huhifadhi tu rekodi za hivi karibuni na kufuta za zamani — ni salama zaidi kwa huduma zinazoendesha muda mrefu. Takwimu zinaonekana kwa fib.cache_info(), na kusafishwa kwa fib.cache_clear(). Hoja za kazi kama hizi lazima ziweze kuhashiwa: orodha kama hoja itasababisha TypeError: unhashable type: 'list'. Na cache inafaa tu kwa kazi safi — zile zinazorudisha kitu kile kile kwa hoja zile zile na hazina athari za pembeni.
Decorator ya property hufanya nini
@property inaruhusu kufikia mbinu kama sifa, bila mabano, na kudhibiti upangaji thamani inapohitajika:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Salio haliwezi kuwa hasi")
self._balance = value
acc = Account(100)
print(acc.balance) # 100
acc.balance = -5 # ValueError
staticmethod na classmethod
class Money:
def __init__(self, kopecks):
self.kopecks = kopecks
@classmethod
def from_rubles(cls, rubles):
return cls(round(rubles * 100))
@staticmethod
def is_valid(value):
return value >= 0
m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1)) # 1250 False
Ni @classmethod inayoruhusu mbinu kufanya kazi na darasa lenyewe, si na mifano yake: from_rubles itaunda kwa usahihi hata kitu cha darasa dogo (subclass).
Decorators za madarasa na madarasa kama decorators
Decorator inaweza kutumika kwa darasa: inapokea darasa na kurudisha darasa (lile lile au lililobadilishwa). Ndivyo @dataclass inavyofanya kazi. Mfano rahisi — rejista ya madarasa:
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(registry) # {'CsvExporter': <class '__main__.CsvExporter'>}
Kinyume chake ni darasa kuwa decorator. Kitu chochote chenye mbinu __call__ kinafaa; hali inahifadhiwa vizuri kwenye sifa za mfano:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def tick():
pass
tick(); tick()
print(tick.calls) # 2
Decorator ya async
Kwa kazi za async def, kifungashio pia lazima kiwe coroutine na kusubiri matokeo kwa await, vinginevyo kitarudisha kitu cha coroutine ambacho hakijatekelezwa:
import functools
def log_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
print(f"Kuanza {func.__name__}")
result = await func(*args, **kwargs)
print(f"Imekamilika {func.__name__}")
return result
return wrapper
Kwa nini decorators zinahitajika katika Python
- Mantiki ya pamoja — kurekodi kumbukumbu, vipimo, kupima muda bila kunakili msimbo kwenye kila kazi.
- Cache —
@functools.cachekwa hesabu ghali na safi. - Ukaguzi — ruhusa, uthibitishaji wa hoja, uidhinishaji katika vishughulikiaji vya wavuti.
- Usajili — njia katika Flask na FastAPI (
@app.get("/")), fixtures za pytest. - Uaminifu — majaribio ya kurudia, muda wa juu wa kusubiri, ushughulikiaji wa makosa.
Faida kuu ni mgawanyo wa majukumu: kazi inashughulikia kazi yake, na tabia ya ziada inaunganishwa kwa mstari mmoja na pia inaondolewa kwa urahisi.
Makosa ya kawaida
- Kifungashio hakirudishi matokeo — kazi baada ya kupambwa inatoa
None. - Hakuna
*args, **kwargs—TypeErrorwakati wa wito kwa hoja. - Kusahau
wraps—__name__na docstring si sahihi, na logi zinachanganya. - Mabano ya kiwanda —
@repeatbadala ya@repeat(3): kazi itaingia kwenye kigezotimes. - Msimbo mzito ndani ya mwili wa decorator — unatekelezwa wakati wa kuingiza moduli, si wakati wa wito.
Hitimisho
Decorators katika Python ni kazi za mpangilio wa juu: zinapokea kazi, zinarudisha kifungashio, na @ ni maandishi mafupi tu ya kazi ya mgawo. Anza na kiolezo chenye *args, **kwargs, return result na functools.wraps; kwa mipangilio ongeza ngazi ya kiwanda. Kazi, closures na type hints ambazo decorators zinategemea zinafundishwa kwa vitendo katika kozi ya Python.
Maswali ya mara kwa mara
Je, decorator inaweza kuondolewa kwenye kazi?
Ikiwa decorator inatumia functools.wraps, asili inapatikana kupitia sifa __wrapped__: func.__wrapped__(...) itaita kazi bila kifungashio. Vinginevyo inabaki tu kuondoa mstari wenye @ kwenye msimbo.
Msimbo wa decorator unatekelezwa lini?
Mwili wa decorator yenyewe unatekelezwa mara moja, wakati kazi inatangazwa, kwa kawaida wakati wa kuingiza moduli. Msimbo ulio ndani ya wrapper unatekelezwa kila mara kazi iliyopambwa inapoitwa.
Je, naweza kupamba mbinu za darasa?
Ndiyo, decorator ya kawaida yenye *args, **kwargs inafanya kazi pia kwa mbinu: self itakuwa kipengele cha kwanza katika args tu.
Decorator inatofautianaje na urithi?
Urithi hubadilisha tabia ya darasa kupitia mfumo wa madarasa, wakati decorator inafunga kazi au darasa mahususi kutoka nje na inachanganyika kwa urahisi na decorators nyingine bila kubadilisha msimbo.
Mada hii inafundishwa kwa kina pamoja na mazoezi katika kozi “Python 3.15: от новичка до профи”.
Nenda kwenye kozi