Decorators & Context Managers

ডেকোরেটর ও কনটেক্সট ম্যানেজার — Python-এর সবচেয়ে elegant প্যাটার্ন

Read: ~30 min Advanced 5 practice problems Live code runner

1. Functions as Values, Functions as Decorators

In Python, functions are first-class — you can pass them around, return them, and store them in variables. A decorator is a function that takes another function and returns a new, modified one. The @ syntax is just syntactic sugar. Decorators let you add features — logging, timing, caching, access control — without touching the original code.

Python-এ ফাংশন first-class — পাস করা যায়, return করা যায়, variable-এ রাখা যায়। Decorator এমন একটি ফাংশন যা অন্য একটি ফাংশন নেয় এবং একটি নতুন, পরিবর্তিত version ফিরিয়ে দেয়। @ সিনট্যাক্স শুধু একটি syntactic sugar। Decorator দিয়ে আপনি মূল কোড না ছুঁয়ে logging, timing, caching, access control যোগ করতে পারেন।

2. A Simple Decorator

basic_deco.py
def announce(fn):
    def wrapper(*args, **kwargs):
        print(f"Calling {fn.__name__}...")
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} returned {result!r}")
        return result
    return wrapper

@announce
def add(a, b):
    return a + b

print(add(3, 4))

# @announce is equivalent to: add = announce(add)

3. Preserving Metadata with functools.wraps

Without help, the wrapper loses the original function's name and docstring. functools.wraps copies them back.

wraps.py
from functools import wraps
import time

def timed(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{fn.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

@timed
def slow_sum(n):
    """Sum 1..n the slow way."""
    return sum(range(n + 1))

print(slow_sum(100000))
print(slow_sum.__name__, "|", slow_sum.__doc__)

4. Decorators with Arguments

deco_args.py
from functools import wraps

def repeat(times):
    def outer(fn):
        @wraps(fn)
        def inner(*args, **kwargs):
            for _ in range(times):
                result = fn(*args, **kwargs)
            return result
        return inner
    return outer

@repeat(3)
def say_hi(name):
    print(f"Hi, {name}!")

say_hi("Ayesha")

5. Context Managers & the with Statement

A context manager defines setup and teardown logic around a block of code. You use one every time you write with open(...). Writing your own is easy — implement __enter__ and __exit__, or use contextlib.contextmanager.

ctx.py
class Timer:
    def __enter__(self):
        import time
        self.t = time.perf_counter()
        return self
    def __exit__(self, exc_type, exc_val, tb):
        import time
        elapsed = time.perf_counter() - self.t
        print(f"elapsed: {elapsed:.4f}s")

with Timer():
    sum(range(1_000_000))

6. contextlib.contextmanager — The Easy Way

contextlib_demo.py
from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")
    try:
        yield
    finally:
        print(f"</{name}>")

with tag("section"):
    print("  This text is inside a tag.")

7. Vocabulary (শব্দভাণ্ডার)

TermMeaningবাংলায়
First-class functionFunctions can be assigned, passed, returned.ফাংশন variable-এ রাখা, pass করা, return করা যায়।
DecoratorFunction that modifies another function.একটি ফাংশনকে পরিবর্তন করে অন্য ফাংশন দেয়।
WrapperThe replacement function returned by a decorator.Decorator-এর return করা নতুন ফাংশন।
Context managerObject with setup/teardown via with.with-এর মাধ্যমে setup/teardown করা object।
__enter__ / __exit__Dunder methods implementing the protocol.Protocol-এর দুটি dunder method।

8. Practice Problems

  1. Write a @log decorator that prints the arguments and return value.
    Argument ও return value প্রিন্ট করে এমন @log decorator লিখুন।
    ✨ Show Answer (উত্তর দেখুন)
    ans1.py
    from functools import wraps
    
    def log(fn):
        @wraps(fn)
        def w(*a, **kw):
            print(f"{fn.__name__}({a}, {kw})")
            r = fn(*a, **kw)
            print(f"  → {r}")
            return r
        return w
    
    @log
    def mul(x, y):
        return x * y
    
    mul(3, 7)
  2. Write a context manager suppress_errors that swallows any exception in its block.
    একটি context manager লিখুন যা ব্লকের যেকোনো exception silently চেপে রাখে।
    ✨ Show Answer (উত্তর দেখুন)
    ans2.py
    from contextlib import contextmanager
    
    @contextmanager
    def suppress_errors():
        try:
            yield
        except Exception as e:
            print("ignored:", e)
    
    with suppress_errors():
        1 / 0
    print("moved on")
  3. Write @retry(times=3) that retries a failing function up to N times.
    @retry(times=3) লিখুন যা ব্যর্থ হলে ফাংশনকে N বার পর্যন্ত retry করবে।
    ✨ Show Answer (উত্তর দেখুন)
    ans3.py
    from functools import wraps
    import random
    
    def retry(times):
        def outer(fn):
            @wraps(fn)
            def inner(*a, **kw):
                for i in range(times):
                    try:
                        return fn(*a, **kw)
                    except Exception as e:
                        print(f"attempt {i+1} failed: {e}")
                raise RuntimeError("all attempts failed")
            return inner
        return outer
    
    @retry(3)
    def flaky():
        if random.random() < 0.7:
            raise ValueError("unlucky")
        return "success"
    
    try:
        print(flaky())
    except RuntimeError as e:
        print(e)
  4. Explain what functools.wraps does — and why it matters.
    functools.wraps কী কাজ করে এবং কেন জরুরি — ব্যাখ্যা করুন।
    ✨ Show Answer (উত্তর দেখুন)

    Answer: Without wraps, the wrapped function loses its original __name__, __doc__, module, and signature — everything introspection tools and debuggers rely on. @wraps(fn) copies that metadata onto the wrapper, so the decorated function looks identical to the original from the outside while still running the added behaviour.

    wraps ছাড়া ফাংশন-এর __name__, __doc__, signature সব হারিয়ে যায়। @wraps(fn) এগুলো wrapper-এ কপি করে — ফলে বাইরে থেকে decorated ফাংশন মূলটির মতোই দেখায়, কিন্তু ভেতরে নতুন behaviour চলে।

  5. Write a context manager that changes the working directory temporarily.
    Temporary-ভাবে directory পরিবর্তন করে এমন context manager লিখুন।
    ✨ Show Answer (উত্তর দেখুন)
    ans5.py
    import os
    from contextlib import contextmanager
    
    @contextmanager
    def chdir(path):
        old = os.getcwd()
        os.chdir(path)
        try:
            yield
        finally:
            os.chdir(old)
    
    with chdir("/tmp"):
        print("inside:", os.getcwd())
    print("outside:", os.getcwd())

Summary — Module 26

Decorators wrap functions to add behaviour without touching the original. Use functools.wraps to preserve metadata. For parameterized decorators, nest one more level. Context managers bracket a block with setup and teardown — write them as classes with __enter__/__exit__, or more easily as generators with @contextmanager. Both are building blocks behind logging, retries, locks, and transaction-style code.

Decorator মূল ফাংশন না ছুঁয়েই behaviour যোগ করে। functools.wraps দিয়ে metadata রক্ষা করুন। Parameter-সহ decorator-এ আরেকটি স্তর nest করুন। Context manager — setup/teardown bracket; class-এ বা @contextmanager generator-এ লিখতে পারেন।

Next Module → Regular Expressions — pattern matching for strings।