[Python] 데코레이터: @decorator, functools.wraps, 클래스 데코레이터
python decorator, @decorator, functools.wraps, decorator with args, class decorator
정의
**데코레이터(decorator)**는 함수/클래스를 받아 다른 함수/클래스를 반환하는 callable이다. @decorator 문법은 단순한 호출 변환에 불과:
@my_decorator
def f(): pass
# 동등
def f(): pass
f = my_decorator(f)
기본 데코레이터
import time
def timing(fn):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
print(f"{fn.__name__}: {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timing
def slow_add(a, b):
time.sleep(0.5)
return a + b
print(slow_add(1, 2))
wrapper는 원래 함수의 행동에 시간 측정을 추가한 새 함수.
functools.wraps: 메타데이터 보존
위의 wrapper는 __name__, __doc__, __module__, __wrapped__ 등이 원본과 달라진다. 디버깅·문서화 도구가 망가짐.
python
from functools import wraps
def timing(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@timing
def add(a, b):
"""두 수 더하기."""
return a + b
print(add.__name__)
print(add.__doc__)
print(add.__wrapped__) 결과
add
두 수 더하기.
<function add at 0x...>모든 데코레이터에 @wraps(fn) 붙이는 게 표준.
인수 받는 데코레이터
데코레이터에 인수가 필요하면 3단 함수가 된다.
from functools import wraps
def retry(times=3):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return fn(*args, **kwargs)
except Exception as e:
if i == times - 1:
raise
print(f"retry {i + 1}: {e}")
return wrapper
return decorator
@retry(times=5)
def flaky():
...
retry(times=5)→decorator반환@decorator→wrapper반환
인수 없이도 호출 가능한 데코레이터
def my_decorator(fn=None, *, prefix="LOG"):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print(prefix, fn.__name__)
return fn(*args, **kwargs)
return wrapper
return decorator if fn is None else decorator(fn)
@my_decorator # 인수 없이
def a(): pass
@my_decorator(prefix="DEBUG") # 인수 있게
def b(): pass
여러 데코레이터 체이닝
@decorator_a
@decorator_b
@decorator_c
def f(): ...
# 동등
f = decorator_a(decorator_b(decorator_c(f)))
아래에서 위로 적용. 위 예시는 c → b → a 순서.
흔한 표준 라이브러리 데코레이터
functools.lru_cache
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2: return n
return fib(n - 1) + fib(n - 2)
fib(100) # 빠르게
fib.cache_info() # 캐시 통계
fib.cache_clear()
@cache (3.9+)는 unbounded lru_cache(maxsize=None).
functools.cached_property
from functools import cached_property
class DataSet:
def __init__(self, data):
self.data = data
@cached_property
def mean(self):
print("computing")
return sum(self.data) / len(self.data)
d = DataSet([1, 2, 3])
d.mean # "computing" 출력 후 2.0
d.mean # 캐시 사용, 출력 없음
인스턴스 속성으로 저장되므로 무효화는 del d.mean.
contextlib.contextmanager
함수 하나로 컨텍스트 매니저 생성.
from contextlib import contextmanager
@contextmanager
def timer(label):
import time
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.4f}s")
with timer("work"):
do_work()
클래스 데코레이터
함수 대신 클래스에 적용. 클래스를 수정·교체하는 데 사용.
def add_repr(cls):
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@add_repr
class User:
def __init__(self, name, age):
self.name = name
self.age = age
print(User("Alice", 30)) # User(name='Alice', age=30)
@dataclass도 본질적으로 이런 클래스 데코레이터.
인스턴스 호출 가능 객체로서의 데코레이터
__call__을 구현한 클래스도 데코레이터로 쓸 수 있다.
class CountCalls:
def __init__(self, fn):
wraps(fn)(self)
self.fn = fn
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.fn(*args, **kwargs)
@CountCalls
def greet():
print("hello")
greet(); greet(); greet()
print(greet.count) # 3
상태가 필요한 데코레이터에 유용.
메서드 데코레이터
인스턴스 메서드에도 적용 가능. self가 첫 인수.
def log_method(fn):
@wraps(fn)
def wrapper(self, *args, **kwargs):
print(f"{type(self).__name__}.{fn.__name__}({args}, {kwargs})")
return fn(self, *args, **kwargs)
return wrapper
class API:
@log_method
def get(self, url): ...
함정과 팁
@wraps빠뜨리지 말 것: stacktrace, help() 망가짐- 데코레이터는 import 시점에 적용: 부작용 주의
- 인수 받는 데코레이터의 시그니처 일관성:
@d와@d()둘 다 지원하면 사용자가 헷갈림 - 타입 힌트 보존:
ParamSpec(3.10+) 활용하면 mypy/pyright가 wrapper 시그니처 추적 가능
from typing import Callable, ParamSpec, TypeVar
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def my_decorator(fn: Callable[P, R]) -> Callable[P, R]:
@wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return fn(*args, **kwargs)
return wrapper
💬 댓글