Quick Reference
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, kwargs):
print(f"calling {func.__name__}")
return func(*args, kwargs)
return wrapper
@log_calls
def greet(name: str) -> str:
return f"Hello, {name}"decorator는 함수·class의 호출 규칙을 바꾸지 않고 logging, 권한, cache처럼 반복 횡단 관심사를 감쌀 때 씁니다. wrapper가 원래 이름·signature·docstring을 잃지 않게 functools.wraps를 쓰고, 상태가 있는 decorator는 호출 사이 공유 범위를 분명히 합니다.
문법
기본형은 단순 데코레이터, 인자를 받는 decorator factory, 클래스 메서드 대상까지 같이 보면 됩니다.
def deco(func):
def wrapper(*args, kwargs):
return func(*args, kwargs)
return wrapper
@deco
def work():
...
def repeat(times):
def deco(func):
def wrapper(*args, **kwargs):
...
return wrapper
return deco
class Service:
@deco
def run(self) -> None:
...@deco는 결국 work = deco(work)를 읽기 좋게 쓴 형태입니다.
문법이 특별해 보여도 본질은 함수를 입력으로 받아 다른 함수로 감싸는 패턴입니다.
감싸기 모델
데코레이터는 함수를 받아 다른 함수로 감싼다
함수 하나를 받아 그 앞뒤에 공통 동작을 끼운 새 함수를 반환하는 패턴입니다.
def deco(func):
def wrapper(*args, kwargs):
print("before")
return func(*args, kwargs)
return wrapper범용 데코레이터라면 원래 함수 시그니처가 제각각일 수 있으므로 wrapper(*args, **kwargs)가 기본 패턴으로 자주 나옵니다.
적용 기준
공통 부가 기능이 여러 함수에 반복되면 데코레이터가 잘 맞는다
로그, 인증, 캐싱, 재시도 같은 코드는 함수마다 본문 맨 위나 아래에 반복되기 쉽습니다. 이럴 때 데코레이터로 감싸면 핵심 로직을 덜 흐리게 만들 수 있습니다.
@log_calls
def fetch_user(user_id: int):
...
@log_calls
def save_user(data: dict):
...반대로 한두 군데만 쓰고 흐름이 단순하면 일반 함수 호출이 더 직관적일 수 있습니다.
def timed(func):
@wraps(func)
def wrapper(*args, kwargs):
return func(*args, kwargs)
return wrapper원래 함수 호출 방식을 최대한 그대로 통과시키는 쪽이 보통 안전합니다.
메타데이터
wraps를 빼면 원래 함수 정보가 흐려진다
데코레이터를 씌우면 함수 이름, docstring, 디버깅 정보가 wrapper 쪽으로 바뀔 수 있습니다.
functools.wraps는 이 문제를 줄여 줍니다.
from functools import wraps
def deco(func):
@wraps(func)
def wrapper(*args, kwargs):
return func(*args, kwargs)
return wrapper테스트, 문서화, 로그 분석에서 차이가 크게 드러나므로 범용 데코레이터라면 거의 습관처럼 붙이는 편이 좋습니다.
주의할 점
wrapper에서 원래 함수의 반환값을 그대로 돌려주지 않으면 데코레이터를 붙인 함수가 None을 반환할 수 있습니다.
# ❌ 결과를 잃어버림
def log_calls(func):
def wrapper(*args, kwargs):
print("call")
func(*args, kwargs)
return wrapper
# ✅ 원래 함수 결과를 반환
def log_calls(func):
def wrapper(*args, kwargs):
print("call")
return func(*args, kwargs)
return wrapper# ❌ 너무 많은 부작용을 숨기면 실행 흐름을 추적하기 어려워진다
@auth_required
@retry
@log_calls
def fetch_user():
...참고 링크
2 sources