মৌলিক

Python-এর decorator: কী এটি, কীভাবে কাজ করে এবং নিজের decorator কীভাবে লিখবেন

হালনাগাদ: · Skillaria.Top

Python-এর decorator হলো এমন একটি ফাংশন, যা অন্য একটি ফাংশন নেয় এবং নতুন একটি ফাংশন ফেরত দেয় — সাধারণত একটি wrapper, যা ডাকার আগে বা পরে অতিরিক্ত আচরণ যোগ করে: লগিং, সময় মাপা, ক্যাশিং, অনুমতি যাচাই। @decorator লেখা def-এর উপরে আসলে func = decorator(func)-এর সংক্ষিপ্ত রূপ; wrapper-এর নাম ও docstring মূল ফাংশনের মতো রাখতে functools.wraps ব্যবহার করা হয়।

Python-এর decorator হলো এমন একটি ফাংশন, যা অন্য একটি ফাংশন নেয় এবং তার বদলে নতুন একটি ফাংশন ফেরত দেয়, সাধারণত একটি wrapper, যা মূল ফাংশনের চারপাশে আচরণ যোগ করে। এভাবে ফাংশনের কোড না বদলেই তার সঙ্গে যুক্ত হয় লগিং, সময় মাপা, ক্যাশ, আবার চেষ্টা করা। Decorator লেখা হয় @নাম দিয়ে, ফাংশন ঘোষণার ঠিক উপরে, এবং এগুলো সব জায়গায় পাওয়া যায়: Flask ও FastAPI-তে, pytest-এ, স্ট্যান্ডার্ড লাইব্রেরিতে।

Decorator সহজ ভাষায়

একটি উপহার ও মোড়ক কাগজের কথা ভাবুন। উপহার (ফাংশন) একই থাকে, কিন্তু বাইরে একটি স্তর যোগ হয়, যেটি আগে কাজ করে। Decorator এমনই একটি মোড়ক: ডাকলে আগে wrapper-এর কোড চলে, সে মূল ফাংশনটি ডাকে এবং ফলাফল নিয়ে কিছু করতে পারে।

এটি কাজ করে কারণ Python-এ ফাংশন সাধারণ অবজেক্ট। তাদের অন্য ফাংশনে পাঠানো যায়, ফাংশন থেকে ফেরত দেওয়া যায়, এবং ফাংশনের ভেতরে ঘোষণা করা যায়। Decorator-এর ভিত্তি এই তিনটি বৈশিষ্ট্য।

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-কে দেখায়, আর মূল ফাংশনটি বাস করে closure-এর ভেতরে — wrapper বাইরের ফাংশনের func ভেরিয়েবলটি মনে রাখে। একটি গুরুত্বপূর্ণ বিষয়: decorator একবারই প্রয়োগ হয়, def চলার সময় — অর্থাৎ মডিউল ইমপোর্টের সময়; আর wrapper চলে প্রতিটি ডাকে।

Closure ও decorator

Closure হলো একটি ভেতরের ফাংশন, যা বাইরের ফাংশন শেষ হওয়ার পরেও তার ভেরিয়েবল মনে রাখে। Closure ছাড়া decorator সম্ভব হতো না: এভাবেই wrapper পায় func-এর অ্যাক্সেস। Closure-এর ভেতরে অবস্থাও রাখা যায়, যেমন ডাকের গণনা:

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 লেখার সার্বজনীন ছাঁচ

প্রথম উদাহরণের দুটি দুর্বলতা: wrapper কোনো আর্গুমেন্ট নেয় না, আর মূল ফাংশনের নাম মুছে দেয়। যে ছাঁচ থেকে যেকোনো 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 — wrapper যেকোনো আর্গুমেন্ট নেয় এবং অপরিবর্তিতভাবে পাঠিয়ে দেয়।
  2. return result — ফল ফেরত দিতে ভুলবেন না, নইলে সাজানো ফাংশন সব সময় None দেবে।
  3. @functools.wraps(func) — মূল ফাংশনের মেটাডেটা wrapper-এ তুলে দেয়।

সময় মাপার একটি 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) থাকলে wrapper পায় মূল ফাংশনের __name__, __doc__, __module__, __qualname__, __annotations__, আর __wrapped__ অ্যাট্রিবিউটে থাকে মূলের সরাসরি রেফারেন্স। এটি ডিবাগিং ও ট্রেসব্যাকের জন্য জরুরি, help()-এর জন্য, নাম ধরে ফাংশন নিবন্ধনকারী ফ্রেমওয়ার্কের জন্য, এবং inspect.signature-এর জন্য, যা প্রকৃত সিগনেচার দেখায়।

প্যারামিটারসহ decorator

কখনো decorator-এর সেটিংস লাগে: @retry(times=3), @app.route("/")। এর জন্য আরেকটি নেস্টিং স্তর যোগ হয়। বাইরের ফাংশন প্যারামিটার নেয় এবং আসল decorator ফেরত দেয় — এই কাঠামোকে বলা হয় 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 — তিন লাইন

ডাকের ক্রম: 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():
    ...

একটি ফাংশনে কয়েকটি decorator

Decorator একটির উপর একটি বসানো যায়। প্রয়োগ হয় নিচ থেকে উপরে: def-এর সবচেয়ে কাছেরটি আগে।

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

# একই কাজ:
report = timer(log_calls(report))

ডাকের সময় বাইরের স্তর (timer) আগে চলে, তাই সে log_calls-এর কাজের সময়ও মাপবে। ক্রম গুরুত্বপূর্ণ: উদাহরণ হিসেবে, একটি decorator অনুমতি যাচাই করলে এবং অন্যটি ফল ক্যাশ করলে ক্রম বদলে ফলাফল বদলে যেতে পারে।

Python-এর স্ট্যান্ডার্ড ও অন্তর্নির্মিত decorator

অনেক কাজের decorator ভাষা ও স্ট্যান্ডার্ড লাইব্রেরিতেই আছে:

Decoratorকী করে
@propertyমেথডকে শুধু-পড়ার অ্যাট্রিবিউটে বদলায়; @x.setter-এর সঙ্গে নিয়ন্ত্রিত অ্যাট্রিবিউট হয়
@staticmethodself-বিহীন ক্লাস-মেথড, ক্লাসের নামস্থানে সাধারণ ফাংশন
@classmethodমেথড ইনস্ট্যান্সের বদলে ক্লাস cls পায় — বিকল্প কনস্ট্রাক্টর এভাবে লেখা হয়
@functools.cache, @functools.lru_cacheআর্গুমেন্ট অনুযায়ী ফল ক্যাশ করে
@functools.wrapsমূল ফাংশনের মেটাডেটা wrapper-এ কপি করে
@dataclasses.dataclassক্লাসের জন্য __init__, __repr__, __eq__ তৈরি করে
@contextlib.contextmanagergenerator থেকে with-এর জন্য context manager বানায়

ক্যাশিং: functools.cache ও lru_cache

ক্যাশ decorator প্রতিটি আর্গুমেন্ট-সেটের ফল মনে রাখে এবং পুনরায় ডাকলে হিসাব না করে তা ফেরত দেয়। ক্লাসিক উদাহরণ — পুনরাবৃত্ত Fibonacci সংখ্যা, যা ক্যাশ ছাড়া সূচকীয় সময় নেয়:

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 সঠিকভাবে সাবক্লাসের অবজেক্টও বানাতে পারে।

Class-এর decorator ও decorator হিসেবে class

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

Async decorator

async def ফাংশনের জন্য wrapper-কেও 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

Decorator কেন ব্যবহার করবেন

  • সর্বত্র থাকা লজিক — লগিং, মেট্রিক্স, সময় মাপা; প্রতিটি ফাংশনে কোড কপি না করে।
  • ক্যাশিং — দামি বিশুদ্ধ হিসাবে @functools.cache।
  • যাচাই — অনুমতি, আর্গুমেন্ট যাচাই, ওয়েব হ্যান্ডলারে অথেন্টিকেশন।
  • নিবন্ধন — Flask ও FastAPI-তে রুট (@app.get("/")), pytest-এর fixture।
  • নির্ভরযোগ্যতা — আবার চেষ্টা, টাইমআউট, ত্রুটি হ্যান্ডলিং।

প্রধান সুবিধা — দায়িত্ব আলাদা রাখা: ফাংশন নিজের কাজ করে, আর সহায়ক আচরণ এক লাইনে যুক্ত হয় এবং সমানভাবে সহজে সরানো যায়।

সাধারণ ভুল

  • wrapper ফল ফেরত দেয় না — সাজানোর পর ফাংশন None দেয়।
  • *args, **kwargs নেই — আর্গুমেন্ট দিয়ে ডাকলে TypeError।
  • wraps ভুলে যাওয়া — ভুল __name__ ও docstring, লগে বিভ্রান্তি।
  • factory-তে বন্ধনী ভুলে যাওয়া — @repeat লিখলে @repeat(3)-এর বদলে ফাংশনটি times প্যারামিটারে চলে যাবে।
  • decorator-এর শরীরে ভারী কোড — সেটি মডিউল ইমপোর্টের সময় চলে, ডাকার সময় নয়।

সারসংক্ষেপ

Python-এর decorator হলো উচ্চ-ক্রমের ফাংশন: ফাংশন নেয়, wrapper ফেরত দেয়, আর @ শুধু একটি সংক্ষিপ্ত অ্যাসাইনমেন্ট। শুরু করুন *args, **kwargs, return result ও functools.wraps-সহ ছাঁচ দিয়ে; সেটিংস লাগলে একটি factory স্তর যোগ করুন। যে ফাংশন, closure ও অ্যানোটেশনের উপর decorator দাঁড়িয়ে আছে, সেগুলো কোর্সে বাস্তব উদাহরণে শেখানো হয়েছে।

সাধারণ প্রশ্ন

Decorator কি সরানো যায়?

যদি decorator functools.wraps ব্যবহার করে, মূল ফাংশন __wrapped__ অ্যাট্রিবিউটে পাওয়া যায়: func.__wrapped__(...) মোড়ক ছাড়া ফাংশনটি ডাকবে। নইলে কোড থেকে @ লাইনটি মুছে ফেলতে হবে।

Decorator-এর কোড কখন চলে?

decorator-এর নিজের শরীর একবার চলে, ফাংশন ঘোষণার সময় — সাধারণত মডিউল ইমপোর্টের সময়। wrapper-এর ভেতরের কোড সাজানো ফাংশন ডাকার প্রতিবার চলে।

ক্লাসের মেথডে decorator লাগানো যায় কি?

হ্যাঁ, *args, **kwargs-সহ সাধারণ decorator মেথডেও কাজ করে: self কেবল args-এর প্রথম উপাদান হয়ে যাবে।

Decorator আর উত্তরাধিকারের পার্থক্য কী?

উত্তরাধিকার ক্লাসের আচরণ বদলায় হায়ারার্কির মাধ্যমে, আর decorator একটি নির্দিষ্ট ফাংশন বা ক্লাসকে বাইরে থেকে মুড়ে দেয় এবং কোড না বদলে অন্য decorator-এর সঙ্গে সহজে মেলানো যায়।

ভিডিওতে বিষয়টি শিখুন

“Python 3.15: от новичка до профи” কোর্সে বিষয়টি অনুশীলনসহ বিস্তারিত শেখানো হয়েছে।

কোর্সে যান