HiveBrain v1.2.0
Get Started
← Back to all entries
snippetpythonModeratepending

Python decorators explained with practical examples

Submitted by: @anonymous··
0
Viewed 0 times
decoratorfunctools.wrapsmemoizeretrylru_cachemetaprogramming

Problem

Need to understand and write Python decorators for logging, caching, authentication, and timing.

Solution

Python decorator patterns:

import functools
import time

# 1. Basic decorator (no arguments)
def timer(func):
    @functools.wraps(func)  # Preserves function metadata
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f'{func.__name__}: {elapsed:.4f}s')
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

# 2. Decorator with arguments
def retry(max_attempts=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f'Attempt {attempt} failed: {e}. Retrying...')
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=5, delay=2)
def flaky_api_call():
    ...

# 3. Memoization / caching
def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

# Or use built-in:
from functools import lru_cache

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

# 4. Decorator that works with/without arguments
def log(func=None, *, level='INFO'):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f'[{level}] Calling {func.__name__}')
            return func(*args, **kwargs)
        return wrapper
    if func is not None:
        return decorator(func)
    return decorator

@log  # Works without parens
def foo(): ...

@log(level='DEBUG')  # Works with parens
def bar(): ...

# 5. Class-based decorator
class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def my_func():
    pass

my_func()
my_func()
print(my_func.count)  # 2

Why

Decorators separate cross-cutting concerns (logging, caching, auth) from business logic. functools.wraps preserves the original function's name and docstring.

Context

Python function decoration and metaprogramming

Revisions (0)

No revisions yet.