Decorators

Decorators wrap functions to extend behavior without modifying the original:

  def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

@log_calls
def greet(name):
    return f"Hello, {name}!"

greet("Alice")
  

Decorators with Arguments

  def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hi():
    print("Hi!")
  

Built-in Decorators

  class User:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @staticmethod
    def validate_email(email):
        return "@" in email

    @classmethod
    def from_dict(cls, data):
        return cls(data["name"])
  

Generators

Yield values lazily, saving memory:

  def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

for num in count_up_to(5):
    print(num)  # 1, 2, 3, 4, 5
  

Generator Expressions

  squares = (x**2 for x in range(1000000))  # memory-efficient
total = sum(x**2 for x in range(100))
  

Context Managers

Manage setup and teardown with with:

  from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f"Took {elapsed:.4f}s")

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

Custom class-based context managers implement __enter__ and __exit__.

Itertools

  from itertools import chain, islice, groupby

list(chain([1, 2], [3, 4]))        # [1, 2, 3, 4]
list(islice(range(100), 5))           # [0, 1, 2, 3, 4]
  

These patterns appear throughout Python frameworks and are essential for writing idiomatic, efficient code.

functools.wraps

Preserve function metadata when wrapping:

  from functools import wraps

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def add(a, b):
    '''Add two numbers.'''
    return a + b

print(add.__name__)  # add (not wrapper)
  

Class-Based Decorators

  class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count}")
        return self.func(*args, **kwargs)

@CountCalls
def process():
    return "done"
  

functools.lru_cache

Memoize expensive pure functions:

  from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)
  

Custom Context Manager Class

  class DatabaseConnection:
    def __enter__(self):
        self.conn = connect_to_db()
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conn.close()
        return False  # do not suppress exceptions

with DatabaseConnection() as db:
    db.execute("SELECT 1")