Python Decorators | @decorator Syntax, functools.wraps, and Patterns

Python Decorators | @decorator Syntax, functools.wraps, and Patterns

이 글의 핵심

Practical guide to Python decorators: @syntax, decorator factories, functools.wraps, and real patterns for logging, caching, and retries.

Introduction

“Dressing up functions”

Decorators are a powerful Python feature for adding behavior around functions (or classes).


1. Function decorators basics

A simple function decorator

def timer(func):
    """Measure how long a function runs."""
    import time
    
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    
    return wrapper

@timer
def slow_function():
    import time
    time.sleep(1)
    return "done"

result = slow_function()
# slow_function took: 1.0012s

Logging decorator

def logger(func):
    """Log function calls."""
    def wrapper(*args, **kwargs):
        print(f"[call] {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[return] {result}")
        return result
    return wrapper

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

add(3, 5)
# [call] add((3, 5), {})
# [return] 8

2. Decorators with arguments

Decorator factory

def repeat(times):
    """Run the wrapped function multiple times."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(times):
                result = func(*args, **kwargs)
                results.append(result)
            return results
        return wrapper
    return decorator

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

print(greet("Alice"))
# ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']

3. Practical decorators

Memoization (caching)

def memoize(func):
    """Cache function results."""
    cache = {}
    
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100))  # very fast!

Authentication decorator

def require_auth(func):
    """Require an authenticated user."""
    def wrapper(user, *args, **kwargs):
        if not user.get('is_authenticated'):
            raise PermissionError("Login required")
        return func(user, *args, **kwargs)
    return wrapper

@require_auth
def delete_post(user, post_id):
    return f"Post {post_id} deleted"

# Usage
user = {'name': 'Alice', 'is_authenticated': True}
print(delete_post(user, 123))  # Post 123 deleted

guest = {'name': 'guest', 'is_authenticated': False}
# delete_post(guest, 123)  # PermissionError!

4. Class decorators

def singleton(cls):
    """Singleton pattern."""
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class Database:
    def __init__(self):
        print("Database connection")
        self.connection = "Connected"

# Usage
db1 = Database()  # Database connection
db2 = Database()  # no extra print (same instance)
print(db1 is db2)  # True

5. functools.wraps

Preserve metadata

from functools import wraps

def my_decorator(func):
    @wraps(func)  # keep original function metadata
    def wrapper(*args, **kwargs):
        """Wrapper docstring."""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """Greeting function."""
    return f"Hello, {name}!"

print(greet.__name__)  # greet (without wraps you'd see wrapper)
print(greet.__doc__)   # Greeting function.

6. Real-world example: API retry decorator

import time
from functools import wraps

def retry(max_attempts=3, delay=1):
    """Retry on failure."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"Attempt {attempt + 1} failed: {e}")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def fetch_data(url):
    import random
    if random.random() < 0.7:
        raise ConnectionError("connection failed")
    return f"data from {url}"

Practical tips

Decorator patterns

# ✅ Stacking multiple decorators
@timer
@logger
@retry(3)
def important_function():
    pass

# Execution order: retry → logger → timer → underlying function

# ✅ Always prefer functools.wraps
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Summary

Key takeaways

  1. Decorators wrap functions to add behavior.
  2. Syntax: @decorator_name above def.
  3. Parameters: use a decorator factory that returns the real decorator.
  4. wraps: preserve __name__, __doc__, and the function module.
  5. Uses: logging, caching, authentication, retries, timing.

Next steps

  • Generators (yield)
  • Flask web basics