அடிப்படை

Python-இல் அலங்கரிப்பாளர்கள் (decorators): அவை என்ன, எப்படி வேலை செய்கின்றன, சொந்தமாக எப்படி எழுதுவது

புதுப்பிக்கப்பட்டது: · Skillaria.Top

Python decorator என்பது மற்றொரு செயல்பாட்டைப் பெற்று புதிய செயல்பாட்டைத் (பொதுவாக ஒரு உறை/wrapper) திருப்பித் தரும் செயல்பாடு; அது அழைப்புக்கு முன் அல்லது பின் நடத்தையைச் சேர்க்கும்: பதிவு செய்தல், நேரம் அளத்தல், தற்காலிக சேமிப்பு, அணுகல் சரிபார்ப்பு. def-க்கு மேல் @decorator எழுதுவது func = decorator(func) என்பதன் சுருக்கம்; உறை அசல் செயல்பாட்டின் பெயரையும் docstring-ஐயும் வைத்துக்கொள்ள functools.wraps பயன்படுகிறது.

Python decorator என்பது ஒரு செயல்பாட்டைப் பெற்று, வேறொரு செயல்பாட்டைத் திருப்பித் தரும் செயல்பாடு — பொதுவாக அசல் செயல்பாட்டைச் சுற்றியுள்ள உறை. இதன் மூலம் செயல்பாட்டின் குறியீட்டை மாற்றாமல் அதற்கு நடத்தையைச் சேர்க்கலாம் — பதிவு செய்தல், நேரம் அளத்தல், தற்காலிக சேமிப்பு, மீண்டும் முயற்சித்தல். Python decorators def-க்கு மேல் @பெயர் என்று எழுதப்படுகின்றன; Flask, FastAPI, pytest மற்றும் நிலையான நூலகத்தில் எங்கும் காணலாம்.

எளிய சொற்களில்: decorator என்றால் என்ன

ஒரு பரிசையும் அதன் பொதி காகிதத்தையும் கற்பனை செய்யுங்கள். பரிசு (செயல்பாடு) அதேதான், ஆனால் வெளியே முதலில் இயங்கும் ஒரு அடுக்கு சேருகிறது. Decorator அத்தகைய பொதி: அழைக்கும்போது முதலில் உறையின் குறியீடு இயங்கும், அது அசல் செயல்பாட்டை அழைக்கும், முடிவோடு ஏதாவது செய்யவும் முடியும்.

இது வேலை செய்வதற்குக் காரணம்: Python-இல் செயல்பாடுகள் சாதாரணப் பொருட்கள். அவற்றை மற்ற செயல்பாடுகளுக்கு அனுப்பலாம், செயல்பாடுகளிலிருந்து திருப்பித் தரலாம், செயல்பாடுகளுக்குள் அறிவிக்கலாம். இந்த மூன்று பண்புகளின் மேல்தான் decorators கட்டப்பட்டுள்ளன.

Decorator எப்படி வேலை செய்கிறது: @ ஒரு தொடரியல் சுருக்கம்

ஒவ்வொரு அழைப்புக்கும் முன்னும் பின்னும் செய்தி அச்சிடும் எளிய decorator-ஐ எடுத்துக்கொள்வோம்:

def log_calls(func):
    def wrapper():
        print(f"அழைக்கிறேன் {func.__name__}")
        result = func()
        print(f"{func.__name__} முடிந்தது")
        return result
    return wrapper

@log_calls
def hello():
    print("வணக்கம்!")

hello()
அழைக்கிறேன் hello
வணக்கம்!
hello முடிந்தது

def-க்கு மேல் உள்ள @log_calls அறிவிப்புக்குப் பின் வரும் இந்த வரிக்குச் சமமானது:

def hello():
    print("வணக்கம்!")

hello = log_calls(hello)

இப்போது hello பெயர் wrapper-ஐச் சுட்டுகிறது; அசல் செயல்பாடு உறைக்குள், மூடலில் வாழ்கிறது — wrapper வெளிப்புறச் செயல்பாட்டின் func மாறியை நினைவில் வைத்திருக்கிறது. முக்கியமான விவரம்: decorator ஒரே ஒருமுறை, def இயங்கும் நேரத்தில், அதாவது தொகுதி இறக்குமதியின்போது பயன்படுகிறது; உறையோ ஒவ்வொரு அழைப்பிலும் இயங்குகிறது.

மூடல்களும் decorators-உம்

மூடல் (closure) என்பது வெளிப்புறச் செயல்பாடு முடிந்த பின்னும் அதன் மாறிகளை நினைவில் வைத்திருக்கும் உள்ளமை செயல்பாடு. மூடல்கள் இல்லாமல் decorators இருக்காது: அப்படித்தான் wrapper func-ஐ அணுகுகிறது. மூடலில் நிலையையும் வைக்கலாம், உதாரணமாக அழைப்புகளின் எண்ணிக்கை:

def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: அழைப்பு எண் {calls}")
        return func(*args, **kwargs)
    return wrapper

Python-இல் decorator எழுதுவது: பொதுவான வார்ப்புரு

முதல் எடுத்துக்காட்டில் இரண்டு குறைகள் உள்ளன: உறை வாதங்களை ஏற்காது, அசல் செயல்பாட்டின் பெயரையும் "விழுங்குகிறது". எந்த decorator-ஐயும் இந்த வேலை செய்யும் வார்ப்புருவிலிருந்து தொடங்குங்கள்:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # அழைப்புக்கு முன் உள்ள குறியீடு
        result = func(*args, **kwargs)
        # அழைப்புக்குப் பின் உள்ள குறியீடு
        return result
    return wrapper

வார்ப்புருவின் மூன்று விதிகள்:

  1. *args, **kwargs — உறை எந்த வாதங்களையும் ஏற்று மாற்றாமல் அனுப்புகிறது.
  2. return result — முடிவைத் திருப்பித் தர மறக்காதீர்கள்; இல்லையென்றால் அலங்கரிக்கப்பட்ட செயல்பாடு எப்போதும் None தரும்.
  3. @functools.wraps(func) — அசல் செயல்பாட்டின் மெட்டாடேட்டாவை உறைக்குக் கொடுக்கிறது.

நேரம் அளக்கும் decorator-க்கு ஓர் எடுத்துக்காட்டு:

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__} எடுத்தது {elapsed:.3f} வி")
        return result
    return wrapper

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

slow_sum(10_000_000)   # slow_sum எடுத்தது 0.142 வி

functools.wraps ஏன் தேவை

wraps இல்லாவிட்டால் அலங்கரிக்கப்பட்ட செயல்பாட்டின் பெயரும் docstring-உம் மற்ற பண்புக்கூறுகளும் மாறிவிடும் — வெளியே wrapper தெரியும்:

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

@plain
def add(a, b):
    """இரண்டு எண்களைக் கூட்டுகிறது."""
    return a + b

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

@functools.wraps(func) உடன் உறைக்கு அசல் செயல்பாட்டின் __name__, __doc__, __module__, __qualname__, __annotations__ கிடைக்கும்; __wrapped__ பண்புக்கூறில் அசலுக்கான குறிப்பும் இருக்கும். இது பிழைத்திருத்தத்துக்கும் traceback-களுக்கும், help()-க்கும், பெயரால் செயல்பாடுகளைப் பதிவு செய்யும் கட்டமைப்புகளுக்கும், உண்மையான கையொப்பத்தைக் காட்டும் inspect.signature-க்கும் முக்கியம்.

அளவுருக்களுடன் decorator (வாதங்களுடன்)

சில நேரங்களில் decorator-க்கு அமைப்புகள் தேவை: @retry(times=3), @app.route("/"). அதற்கு ஒரு கூடுதல் உள்ளமை அடுக்கு சேர்க்கப்படுகிறது. வெளிப்புறச் செயல்பாடு அளவுருக்களை ஏற்று உண்மையான decorator-ஐத் திருப்பும் — அத்தகைய அமைப்பு decorator தொழிற்சாலை எனப்படும்.

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 — மூன்று வரிகள்

அழைப்பின் வரிசை: repeat(times=3) decorator-ஐத் திருப்பும்; அது ping-க்குப் பொருந்தி wrapper-ஐத் திருப்பும். வேறு வார்த்தைகளில், @repeat(times=3) என்பது ping = repeat(times=3)(ping).

நடைமுறை எடுத்துக்காட்டு — பிழையில் மீண்டும் முயற்சித்தல்:

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

Decorators ஒன்றுக்கு மேல் ஒன்றாக வைக்கப்படலாம். அவை கீழிருந்து மேலாகப் பொருந்தும்: def-க்கு அருகில் உள்ளது முதலில்.

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

# அதே பொருள்:
report = timer(log_calls(report))

அழைக்கும்போது வெளிப்புற அடுக்கு (timer) முதலில் இயங்குகிறது; அதனால் அது log_calls-இன் நேரத்தையும் அளக்கும். வரிசை முக்கியம்: உதாரணமாக ஒரு decorator அணுகல் உரிமையைச் சரிபார்த்து, மற்றொன்று முடிவைத் தற்காலிகமாகச் சேமித்தால்.

Python-இன் நிலையான மற்றும் உள்ளமைவு decorators

பயனுள்ள பல decorators ஏற்கனவே மொழியிலும் நிலையான நூலகத்திலும் உள்ளன:

Decoratorஎன்ன செய்கிறது
@propertyமுறையை படிக்க மட்டும் பண்புக்கூறாக மாற்றுகிறது; @x.setter உடன் கட்டுப்படுத்தப்பட்ட பண்புக்கூறாக
@staticmethodself இல்லாத வகுப்பு முறை; வகுப்பின் பெயர்வெளியில் சாதாரண செயல்பாடு
@classmethodமுறை நிகழ்வுக்கு பதிலாக வகுப்பு cls-ஐப் பெறுகிறது — மாற்று constructor-களுக்கு இதுவே பயன்படும்
@functools.cache, @functools.lru_cacheவாதங்களின்படி முடிவுகளைத் தற்காலிகமாகச் சேமிக்கின்றன
@functools.wrapsசெயல்பாட்டின் மெட்டாடேட்டாவை உறைக்கு நகலெடுக்கிறது
@dataclasses.dataclassவகுப்புக்கு __init__, __repr__, __eq__ உருவாக்குகிறது
@contextlib.contextmanagerஒரு generator-ஐ with-க்கான context manager ஆக்குகிறது

தற்காலிக சேமிப்பு: functools.cache மற்றும் lru_cache

தற்காலிக சேமிக்கும் decorator ஒவ்வொரு வாதத் தொகுப்புக்கும் முடிவை நினைவில் வைத்து, மீண்டும் அழைக்கப்படும்போது கணக்கிடாமல் அதைத் தரும். உன்னதமான எடுத்துக்காட்டு — ஃபிபோனாச்சி எண்கள்; சேமிப்பு இல்லாமல் அவை அதிவேகமாக நீண்ட நேரம் எடுக்கும்:

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 (Python 3.9-இல் வந்தது) எல்லா முடிவுகளையும் வரம்பின்றி வைத்திருக்கும். @functools.lru_cache(maxsize=128) சமீபத்திய பதிவுகளை மட்டும் வைத்து பழையவற்றை நீக்கும் — நீண்ட நேரம் இயங்கும் சேவைகளுக்கு இது பாதுகாப்பானது. புள்ளிவிவரங்களை fib.cache_info() மூலம் பார்க்கலாம், fib.cache_clear() மூலம் அழிக்கலாம். இத்தகைய செயல்பாடுகளின் வாதங்கள் ஹாஷ் செய்யக்கூடியதாக இருக்க வேண்டும்: பட்டியலை வாதமாகக் கொடுத்தால் TypeError: unhashable type: 'list' வரும். மேலும் தூய செயல்பாடுகளை மட்டுமே சேமியுங்கள் — ஒரே வாதங்களுக்கு எப்போதும் ஒரே முடிவைத் தருபவை, பக்க விளைவுகள் இல்லாதவை.

property decorator என்ன செய்கிறது

@property ஒரு முறையை அடைப்புகள் இல்லாமல் பண்புக்கூறாகப் பயன்படுத்த அனுமதிக்கும்; தேவைப்பட்டால் ஒதுக்கலைக் கட்டுப்படுத்தும்:

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("இருப்பு எதிர்மறையாக இருக்க முடியாது")
        self._balance = value

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

staticmethod மற்றும் 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

@classmethod தான் ஒரு முறையை நிகழ்வுகளுக்குப் பதிலாக வகுப்பிலேயே வேலை செய்ய அனுமதிக்கிறது: from_rubles துணைவகையின் பொருளையும் சரியாக உருவாக்கும்.

வகுப்புகளின் decorators மற்றும் decorator-ஆக வகுப்புகள்

Decorator-ஐ ஒரு வகுப்புக்கும் பயன்படுத்தலாம்: அது வகுப்பைப் பெற்று அதே அல்லது மாற்றப்பட்ட வகுப்பைத் திருப்பும். @dataclass இப்படித்தான் வேலை செய்கிறது. எளிய எடுத்துக்காட்டு — வகுப்புகளின் பதிவேடு:

registry = {}

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

@register
class CsvExporter:
    pass

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

எதிர்த் திசையில், ஒரு வகுப்பு decorator-ஆகப் பயன்படும். __call__ முறை உள்ள எந்தப் பொருளும் பொருந்தும்; நிலையை நிகழ்வின் பண்புக்கூறுகளில் வைப்பது வசதியானது:

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

async def செயல்பாடுகளுக்கு உறையும் ஒரு coroutine ஆக இருக்க வேண்டும், முடிவை await மூலம் காத்திருக்க வேண்டும்; இல்லையென்றால் இயங்காத coroutine பொருள் திரும்பும்:

import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"தொடக்கம் {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"தயார் {func.__name__}")
        return result
    return wrapper

Python-இல் decorators எதற்காக

  • பொதுவான தர்க்கம் — பதிவு செய்தல், அளவீடுகள், நேர அளவீடு; ஒவ்வொரு செயல்பாட்டிலும் குறியீட்டை நகலெடுக்காமல்.
  • தற்காலிக சேமிப்பு — விலையுயர்ந்த தூய கணக்கீடுகளுக்கு @functools.cache.
  • சரிபார்ப்புகள் — அணுகல் உரிமைகள், வாதங்களின் சரிபார்ப்பு, இணையக் கையாளுநர்களில் அங்கீகாரம்.
  • பதிவு செய்தல் — Flask மற்றும் FastAPI-இல் வழிகள் (@app.get("/")), pytest fixtures.
  • நம்பகத்தன்மை — மீண்டும் முயற்சிகள், நேர வரம்புகள், பிழை கையாளுதல்.

முக்கிய நன்மை — பொறுப்புகளின் பிரிப்பு: செயல்பாடு தன் வேலையைச் செய்கிறது, துணை நடத்தை ஒரு வரியில் இணைக்கப்படுகிறது, அதே போல எளிதாக நீக்கப்படுகிறது.

பொதுவான தவறுகள்

  • உறை முடிவைத் திருப்பவில்லை — அலங்கரித்த பின் செயல்பாடு None தருகிறது.
  • *args, **kwargs இல்லை — வாதங்களுடன் அழைக்கும்போது TypeError.
  • wraps மறந்தது — தவறான __name__ மற்றும் docstring, பதிவுகளில் குழப்பம்.
  • தொழிற்சாலையில் அடைப்புகள் — @repeat(3)-க்குப் பதிலாக @repeat: செயல்பாடு times அளவுருவுக்குள் போய்விடும்.
  • decorator உடலில் கனமான குறியீடு — அது அழைப்பின்போது அல்ல, தொகுதி இறக்குமதியின்போதே இயங்கும்.

சுருக்கம்

Python decorators உயர்-வரிசை செயல்பாடுகள்: அவை ஒரு செயல்பாட்டைப் பெற்று உறையைத் திருப்பும்; @ என்பது ஒதுக்கலுக்கான ஒரு குறுகிய வடிவம் மட்டுமே. *args, **kwargs, return result, functools.wraps கொண்ட வார்ப்புருவிலிருந்து தொடங்குங்கள்; அமைப்புகள் தேவைப்பட்டால் தொழிற்சாலை அடுக்கைச் சேர்க்கலாம். Decorators-க்கு அடிப்படையான செயல்பாடுகள், மூடல்கள் மற்றும் annotations Python பாடநெறியில் நடைமுறையில் விளக்கப்பட்டுள்ளன.

அடிக்கடி கேட்கப்படும் கேள்விகள்

decorator-ஐ ஒரு செயல்பாட்டிலிருந்து நீக்க முடியுமா?

decorator <code>functools.wraps</code> பயன்படுத்தினால் அசல் <code>__wrapped__</code> பண்புக்கூறில் கிடைக்கும்: <code>func.__wrapped__(...)</code> உறை இல்லாமல் அழைக்கும். இல்லையென்றால் குறியீட்டிலிருந்து <code>@</code> வரியை நீக்குவதுதான் வழி.

decorator-இன் குறியீடு எப்போது இயங்கும்?

decorator-இன் உடல் ஒருமுறை மட்டும், செயல்பாடு அறிவிக்கப்படும்போது, பொதுவாக தொகுதி இறக்குமதியின்போது இயங்கும். wrapper-க்குள் உள்ள குறியீடு அலங்கரிக்கப்பட்ட செயல்பாடு அழைக்கப்படும் ஒவ்வொரு முறையும் இயங்கும்.

வகுப்பின் முறைகளுக்கும் decorators பயன்படுத்தலாமா?

ஆம், *args, **kwargs கொண்ட சாதாரண decorator முறைகளுக்கும் வேலை செய்யும்: <code>self</code> வெறுமனே args-இன் முதல் உறுப்பாக இருக்கும்.

decorator மற்றும் மரபுரிமை (inheritance) இடையே வேறுபாடு என்ன?

மரபுரிமை படிநிலை மூலம் வகுப்பின் நடத்தையை மாற்றுகிறது; decorator குறிப்பிட்ட செயல்பாட்டையோ வகுப்பையோ வெளியிலிருந்து சுற்றுகிறது; குறியீட்டை மாற்றாமல் மற்ற decorators-உடன் எளிதாக இணைகிறது.

இந்தத் தலைப்பை வீடியோவில் கற்றுக்கொள்ளுங்கள்

“Python 3.15: от новичка до профи” பாடநெறியில் இந்தத் தலைப்பு பயிற்சியுடன் விரிவாக விளக்கப்பட்டுள்ளது.

பாடநெறிக்குச் செல்