Python decorator 동작 원리 이해
A deep-dive into how Python decorators work under the hood — from basic function decorators to class decorators, stacking, and practical patterns.
Python decorator 동작 원리 이해
Introduction
Decorators are one of Python's most elegant features, but they are also one of the most misunderstood. Many developers use decorators daily — @app.route in Flask, @login_required in Django, @property in classes — without truly understanding what happens when Python encounters the @ symbol.
I spent time deeply understanding decorators while refactoring a trading system in Lisbon. The codebase was littered with poorly written decorators that were causing subtle bugs — modified function signatures, lost metadata, and unexpected side effects. Understanding how decorators work at a fundamental level was essential to fixing these issues.
Environment
Python 3.12.3Problem
Problem 1: Decorator destroys function metadata
import functools
def my_decorator(func):
def wrapper(*args, **kwargs):
"""Wrapper documentation"""
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone by name."""
return f"Hello, {name}"
print(greet.__name__) # "wrapper" — not "greet"!
print(greet.__doc__) # "Wrapper documentation" — not the original!Output:
wrapper
Wrapper documentationProblem 2: Decorator with arguments is confusing
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(times=3) # What is happening here?
def say_hello():
print("Hello")
# Equivalent to: say_hello = repeat(times=3)(say_hello)
# Which means: say_hello = decorator(say_hello)
# Which means: say_hello = wrapperProblem 3: Class-based decorator fails
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello")
# This works:
say_hello() # Prints "Hello"
print(say_hello.count) # 1
# But isinstance check fails:
print(isinstance(say_hello, CountCalls)) # True
print(type(say_hello)) # — OK Problem 4: Stacked decorators execute in wrong order
def decorator_a(func):
def wrapper(*args, **kwargs):
print("A before")
result = func(*args, **kwargs)
print("A after")
return result
return wrapper
def decorator_b(func):
def wrapper(*args, **kwargs):
print("B before")
result = func(*args, **kwargs)
print("B after")
return result
return wrapper
@decorator_a
@decorator_b
def say_hello():
print("Hello")
say_hello()
# Output:
# A before
# B before
# Hello
# B after
# A after
# A wraps B, not the other way aroundAnalysis
A decorator is simply a function that takes a function and returns a new function. The @ syntax is syntactic sugar for function composition.
How @decorator works:
@decorator
def function():
pass
# Is equivalent to:
def function():
pass
function = decorator(function)The three layers of decorators:
- Simple decorator: Takes a function, returns a wrapper
- Decorator factory: Takes arguments, returns a decorator
- Class decorator: Uses
__init__and__call__instead of nested functions
Why function metadata is lost:
When you return wrapper from a decorator, the decorated function becomes wrapper, which has wrapper's name, docstring, and module. functools.wraps copies the original function's metadata to the wrapper.
Solution
Fix 1: Always use functools.wraps
import functools
def my_decorator(func):
@functools.wraps(func) # This is critical!
def wrapper(*args, **kwargs):
"""Wrapper documentation"""
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone by name."""
return f"Hello, {name}"
print(greet.__name__) # "greet" — correct!
print(greet.__doc__) # "Greet someone by name." — correct!Fix 2: Decorator with arguments
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hello():
print("Hello")
say_hello() # Prints "Hello" 3 times
print(say_hello.__name__) # "say_hello" — preserved!Fix 3: Class-based decorator
import functools
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)
def reset(self):
self.count = 0
@CountCalls
def say_hello():
"""Say hello."""
print("Hello")
say_hello() # Prints "Hello"
say_hello() # Prints "Hello"
print(say_hello.count) # 2
print(say_hello.__name__) # "say_hello"
say_hello.reset() # Reset counterFix 4: Practical decorator patterns
import functools
import time
import logging
def timer(func):
"""Measure execution time."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
logging.info(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def retry(max_attempts=3, delay=1):
"""Retry on exception."""
def decorator(func):
@functools.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
logging.warning(f"{func.__name__} failed (attempt {attempt + 1}): {e}")
time.sleep(delay)
return wrapper
return decorator
def cache(maxsize=128):
"""Simple LRU cache."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
if not hasattr(wrapper, '_cache'):
wrapper._cache = {}
if key in wrapper._cache:
return wrapper._cache[key]
result = func(*args, **kwargs)
wrapper._cache[key] = result
return result
return wrapper
return decoratorLessons Learned
- Always use
@functools.wraps(func)in your decorators to preserve function metadata. - Understand the three layers: simple decorator, decorator factory (with arguments), class decorator.
- Stacked decorators execute bottom-up — the bottom decorator wraps first.
- Use
functools.update_wrapperin class-based decorators for the same effect. - Keep decorators simple and focused — complex logic should be in the decorated function, not the decorator.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.