Batayan

Mga decorator sa Python: ano ang mga ito, paano gumagana, at paano gumawa ng sarili

Na-update: · Skillaria.Top

Ang decorator sa Python ay function na tumatanggap ng ibang function at nagbabalik ng bago, karaniwang wrapper, na nagdaragdag ng gawi bago o pagkatapos ng tawag: logging, pagsukat ng oras, caching, o pagsusuri ng pahintulot. Ang @decorator sa ibabaw ng def ay shortcut ng func = decorator(func); para mapanatili ng wrapper ang pangalan at docstring ng orihinal na function, gamitin ang functools.wraps.

Ang decorator sa Python ay function na tumatanggap ng isa pang function at nagbabalik ng bago — kadalasan ay wrapper sa paligid ng orihinal. Sa ganitong paraan, nadadagdagan ng gawi ang isang function — logging, pagsukat ng oras, cache, o muling pagsubok — nang hindi binabago ang code nito. Isinusulat ang mga decorator sa Python sa pamamagitan ng linyang @pangalan sa itaas ng deklarasyon ng function, at makikita ang mga ito kahit saan: sa Flask at FastAPI, sa pytest, at sa standard library.

Ano ang decorator sa simpleng salita

Isipin ang regalo at ang pambalot nito. Nananatiling pareho ang regalo (function), pero may patong sa labas na unang gumagana. Ganyan ang decorator: kapag tinawag ang function, unang tatakbo ang code ng wrapper, tatawagin nito ang orihinal na function, at maaari nitong gawin ang anuman sa resulta.

Gumagana ito dahil sa Python, ordinaryong object ang mga function. Maaari silang ipasa sa ibang function, ibalik mula sa function, at ideklara sa loob ng function. Nakabatay ang mga decorator sa tatlong katangiang ito.

Paano gumagana ang decorator: ang @ ay syntactic sugar

Narito ang pinakasimpleng decorator, na nagpi-print ng mensahe bago at pagkatapos ng tawag:

def log_calls(func):
    def wrapper():
        print(f"Tinatawag ang {func.__name__}")
        result = func()
        print(f"Natapos ang {func.__name__}")
        return result
    return wrapper

@log_calls
def hello():
    print("Kumusta!")

hello()
Tinatawag ang hello
Kumusta!
Natapos ang hello

Ang @log_calls sa itaas ng def ay ganap na katumbas ng linyang nasa ilalim ng deklarasyon:

def hello():
    print("Kumusta!")

hello = log_calls(hello)

Ngayon, ang pangalang hello ay tumuturo na sa wrapper, at nabubuhay ang orihinal na function sa loob ng closure — naaalala ng wrapper ang variable na func mula sa panlabas na function. Mahalagang detalye: isang beses lang nailalapat ang decorator, sa sandaling tumakbo ang def, ibig sabihin kapag na-import ang module; pero tumatakbo ang wrapper sa bawat tawag.

Mga closure at decorator

Ang closure ay nested na function na naaalala ang mga variable ng panlabas na function kahit tapos na ito. Kung walang closure, walang decorator: sa ganitong paraan nakukuha ng wrapper ang access sa func. Maaari ring mag-imbak ng estado sa closure, gaya ng counter ng tawag:

def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: tawag blg. {calls}")
        return func(*args, **kwargs)
    return wrapper

Paano gumawa ng decorator sa Python: pangkalahatang template

May dalawang kahinaan ang unang halimbawa: hindi tumatanggap ng argumento ang wrapper, at "nilalamon" nito ang pangalan ng orihinal na function. Ito ang gumaganang template na dapat simulan sa anumang decorator:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # code bago tawagin
        result = func(*args, **kwargs)
        # code pagkatapos tawagin
        return result
    return wrapper

Tatlong tuntunin ng template:

  1. *args, **kwargs — tumatanggap ang wrapper ng anumang argumento at ipinapasa ang mga ito nang hindi binabago.
  2. return result — huwag kalimutang ibalik ang resulta, kung hindi, laging None ang ibibigay ng decorated na function.
  3. @functools.wraps(func) — ilipat ang metadata ng orihinal na function sa wrapper.

Halimbawa ng decorator — pagsukat ng oras ng pagpapatakbo:

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"Tumagal ng {elapsed:.3f} s ang {func.__name__}")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # Tumagal ng 0.142 s ang slow_sum

Bakit kailangan ang functools.wraps

Kung wala ang wraps, mapapalitan ng decorated na function ang pangalan at docstring nito — makikita ang wrapper sa labas:

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """Nagdaragdag ng dalawang numero."""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None

Sa @functools.wraps(func), nakukuha ng wrapper ang __name__, __doc__, __module__, __qualname__, at __annotations__ ng orihinal, at nakatago sa attribute na __wrapped__ ang reference sa orihinal. Mahalaga ito para sa debugging at traceback, para sa help(), para sa mga framework na nagre-rehistro ng function ayon sa pangalan, at para sa inspect.signature, na nagpapakita ng tunay na signature.

Decorator na may parameter (na may argumento)

Minsan kailangan ng decorator ng mga setting: @retry(times=3), @app.route("/"). Para dito, nagdaragdag ng isa pang antas ng nesting. Tumatanggap ang panlabas na function ng mga parameter at nagbabalik ng tunay na decorator — ang ganitong konstruksiyon ay tinatawag na 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 — tatlong linya

Pagkakasunod ng mga tawag: ibinabalik ng repeat(times=3) ang decorator, na inilalapat sa ping at nagbabalik ng wrapper. Sa madaling sabi, ang @repeat(times=3) ay katumbas ng ping = repeat(times=3)(ping).

Praktikal na halimbawa — muling pagsubok kapag may error:

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

Maraming decorator sa iisang function

Maaaring magkasunod-sunod na ilagay ang mga decorator. Inilalapat ang mga ito mula ibaba pataas: ang pinakamalapit sa def ang unang nailalapat.

@timer
@log_calls
def report():
    ...

# katumbas nito:
report = timer(log_calls(report))

Sa pagtawag, unang tumatakbo ang panlabas na layer (timer), kaya masusukat din nito ang oras ng log_calls. Mahalaga ang pagkakasunod, halimbawa kung ang isang decorator ay sumusuri ng pahintulot at ang isa pa ay nag-cache ng resulta.

Mga karaniwan at built-in na decorator ng Python

Marami nang kapaki-pakinabang na decorator sa wika at sa standard library:

DecoratorGinagawa
@propertyginagawang read-only na attribute ang method; kasama ng @x.setter, nagiging kontroladong attribute
@staticmethodmethod ng klase na walang self, ordinaryong function sa namespace ng klase
@classmethodtumatanggap ang method ng klase na cls sa halip na instance — ganito isinusulat ang mga alternatibong constructor
@functools.cache, @functools.lru_cachenagca-cache ng resulta ayon sa argumento
@functools.wrapskinokopya ang metadata ng function sa wrapper
@dataclasses.dataclassbumubuo ng __init__, __repr__, __eq__ para sa klase
@contextlib.contextmanagerginagawang context manager para sa with ang generator

Caching: functools.cache at lru_cache

Ang caching decorator ay nagtatandaan ng resulta para sa bawat hanay ng argumento, at sa susunod na tawag ay ibinabalik ito nang hindi na kinakalkula. Klasikong halimbawa ang recursive na Fibonacci, na kung walang cache ay napakabagal ng pagkalkula — exponential ang oras:

import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075

Iniimbak ng functools.cache (lumitaw sa Python 3.9) ang lahat ng resulta nang walang limitasyon. Ang @functools.lru_cache(maxsize=128) ay nagtatago lamang ng pinakahuling mga entry at inaalis ang mga luma — mas ligtas ito para sa matagal nang tumatakbong serbisyo. Makikita ang estadistika gamit ang fib.cache_info(), at nililinis gamit ang fib.cache_clear(). Dapat hashable ang mga argumento ng ganitong function: ang list bilang argumento ay magbibigay ng TypeError: unhashable type: 'list'. Ang caching ay dapat gamitin lamang sa mga pure function — yaong laging nagbabalik ng parehong resulta sa parehong argumento at walang side effect.

Ano ang ginagawa ng decorator na property

Pinapayagan ng @property na tawagin ang method na parang attribute, nang walang panaklong, at kung kinakailangan, kontrolin ang pag-assign:

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("Hindi maaaring negatibo ang balanse")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError

staticmethod at 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

Ang @classmethod ang nagbibigay-daan sa method na gumana sa mismong klase at hindi sa mga instance nito: tamang makakalikha ang from_rubles ng object ng subclass.

Mga decorator ng klase at mga klaseng decorator

Maaaring ilapat ang decorator sa klase: tumatanggap ito ng klase at nagbabalik ng klase (ang parehong klase o binago). Ganito gumagana ang @dataclass. Simpleng halimbawa — isang registry ng mga klase:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': <class '__main__.CsvExporter'>}

Ang kabaligtaran ay klase bilang decorator. Puwede ang anumang object na may method na __call__; kapaki-pakinabang na itago ang estado sa mga attribute ng instance:

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

Asynchronous na decorator

Para sa mga function na async def, dapat ding maging coroutine ang wrapper at maghintay ng resulta gamit ang await; kung hindi, ibabalik nito ang hindi pa natatapos na coroutine object:

import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"Nagsimula ang {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"Tapos na ang {func.__name__}")
        return result
    return wrapper

Bakit gumagamit ng decorator sa Python

  • Cross-cutting na lohika — logging, metrics, at pagsukat ng oras nang hindi kinokopya ang code sa bawat function.
  • Caching — @functools.cache para sa mamahaling pure na kalkulasyon.
  • Pagsusuri — pahintulot, pagbeberipika ng argumento, authorization sa web handler.
  • Pagre-rehistro — mga ruta sa Flask at FastAPI (@app.get("/")), mga fixture ng pytest.
  • Katatagan — muling pagsubok, timeout, pangangasiwa ng error.

Pangunahing bentahe ang paghihiwalay ng responsibilidad: ginagawa ng function ang sariling gawain, at isinasaksak ang karagdagang gawi sa isang linya, at ganoon din kadali itong alisin.

Karaniwang pagkakamali

  • Hindi ibinabalik ng wrapper ang resulta — None ang ibinibigay ng function pagkatapos ma-decorate.
  • Walang *args, **kwargs — TypeError kapag tinawag na may argumento.
  • Nakalimutan ang wraps — maling __name__ at docstring, at kalituhan sa mga log.
  • Nakalimutan ang panaklong sa factory — @repeat sa halip na @repeat(3): mapupunta ang function sa parameter na times.
  • Mabigat na code sa katawan ng decorator — tumatakbo ito sa pag-import ng module, hindi sa pagtawag.

Buod

Ang mga decorator sa Python ay higher-order function: tumatanggap ng function, nagbabalik ng wrapper, at ang @ ay maikling anyo lamang ng assignment. Magsimula sa template na may *args, **kwargs, return result, at functools.wraps; para sa mga setting, magdagdag ng antas ng factory. Ang mga function, closure, at annotation na pinagbabatayan ng mga decorator ay tinalakay nang praktikal sa kurso ng Python.

Mga madalas itanong

Maaari bang alisin ang decorator sa isang function?

Kung gumagamit ang decorator ng functools.wraps, nasa attribute na __wrapped__ ang orihinal: ang func.__wrapped__(...) ay tatawag sa function nang walang wrapper. Kung hindi, kailangang tanggalin ang linyang may @ sa code.

Kailan tumatakbo ang code ng decorator?

Ang katawan mismo ng decorator ay tumatakbo nang isang beses, sa deklarasyon ng function, kadalasan habang ina-import ang module. Ang code sa loob ng wrapper ay tumatakbo sa bawat tawag ng decorated na function.

Maaari bang i-decorate ang mga method ng klase?

Oo, gumagana ang karaniwang decorator na may *args, **kwargs kahit sa mga method; ang self ay magiging unang elemento lamang ng args.

Ano ang pagkakaiba ng decorator at inheritance?

Binabago ng inheritance ang gawi ng klase sa pamamagitan ng hierarchy, samantalang binabalot ng decorator ang partikular na function o klase mula sa labas, at madaling pagsamahin sa ibang decorator nang hindi binabago ang code.

Pag-aralan ito sa video

Detalyadong tinatalakay ang paksang ito, may praktis, sa kursong “Python 3.15: от новичка до профи”.

Pumunta sa kurso