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
- Decorators wrap functions to add behavior.
- Syntax:
@decorator_nameabovedef. - Parameters: use a decorator factory that returns the real decorator.
- wraps: preserve
__name__,__doc__, and the function module. - Uses: logging, caching, authentication, retries, timing.
Next steps
- Generators (
yield) - Flask web basics