Python 데코레이터 설명
python decorator 가 너무 헷갈려
좋아, 데코레이터를 “마법”이 아니라 “함수로 함수를 꾸미는 평범한 기술”로 풀어서 볼게. 차근차근 따라가면 헷갈림이 사라진다.
1) 한 문장 정의
데코레이터는 “함수를 받아서 새로운 함수를 반환하는 함수”다. @something은 단지 설탕 문법(syntax sugar)이고, 실제로는 func = something(func)과 같다.
2) 가장 작은 예제부터
먼저 아무 장식도 없는 함수.
def greet(name):
return f"Hello, {name}"
이제 이 함수를 받아 살짝 바꾸는 “데코레이터 함수”를 만든다.
def shout_decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs) # 원래 함수 실행
return result.upper() # 결과를 변형
return wrapper
적용은 두 가지 방법이 있다.
# 방법 A: @ 문법
@shout_decorator
def greet(name):
return f"Hello, {name}"
# 방법 B: 수동 적용 (동치)
def greet(name):
return f"Hello, {name}"
greet = shout_decorator(greet)
이제 greet("world")는 "HELLO, WORLD"를 반환한다.
핵심 포인트는 wrapper가 원본 함수를 감싸고 있다는 것.
3) 꼭 쓰는 도구: functools.wraps
데코레이터를 쓰면 원래 함수의 이름, docstring이 wrapper로 바뀌어 버린다. 이를 바로잡는 게 functools.wraps.
from functools import wraps
def shout_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
이제 greet.__name__, help(greet) 등이 원본과 일치한다. 실무에선 wraps를 항상 쓴다고 생각하자.
4) 인자가 있는 데코레이터
가끔 데코레이터 자체에도 옵션이 필요하다. 이때는 “함수를 만드는 함수”를 한 번 더 감싼다.
from functools import wraps
def repeat(times=2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last = None
for _ in range(times):
last = func(*args, **kwargs)
return last
return wrapper
return decorator
@repeat(times=3)
def ping():
print("ping")
정신승리 포인트: @repeat(3)는 즉시 repeat(3)을 호출해 “진짜 데코레이터(decorator)”를 만든 다음 그게 ping을 감싼다.
5) 적용 순서(스택)
여러 데코레이터를 쌓으면, 코드에선 위에서 아래로 읽히지만 적용은 아래에서 위로 된다.
@A
@B
def f(): ...
이는 f = A(B(f))와 같다. 즉 B가 먼저 감싸고, 그 다음 A가 감싼다.
6) 자주 쓰는 패턴들
로그/타이밍
import time
from functools import wraps
def timeit(func):
@wraps(func)
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
dt = time.perf_counter() - t0
print(f"{func.__name__} took {dt:.4f}s")
return wrapper
메모이제이션(이미 표준 라이브러리에 있음)
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
권한 체크(개념 예시)
from functools import wraps
def require_role(role):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if role not in user.roles:
raise PermissionError("nope")
return func(user, *args, **kwargs)
return wrapper
return decorator
프로퍼티/정적/클래스 메서드(내장 데코레이터)
class Foo:
@staticmethod
def s(): ...
@classmethod
def c(cls): ...
@property
def name(self): ...
클래스 데코레이터(클래스를 받아 클래스/인스턴스를 돌려줌)
def add_repr(cls):
def __repr__(self):
fields = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({fields})"
cls.__repr__ = __repr__
return cls
@add_repr
class Point:
def __init__(self, x, y): self.x, self.y = x, y
7) 비동기 함수 꾸미기(중요 포인트)
async def 함수를 감쌀 땐 wrapper도 async def여야 await를 안전하게 전달할 수 있다.
from functools import wraps
import asyncio
def async_timeit(func):
@wraps(func)
async def wrapper(*args, **kwargs):
t0 = asyncio.get_event_loop().time()
try:
return await func(*args, **kwargs) # await 잊지 말기
finally:
dt = asyncio.get_event_loop().time() - t0
print(f"{func.__name__} took {dt:.4f}s")
return wrapper
동기/비동기 둘 다 지원하려면 오버로드하거나, 감싸기 전에 asyncio.iscoroutinefunction(func)로 분기한다.
8) 흔한 함정들 체크리스트
함수 시그니처 손실 문제: wraps로 메타데이터는 살리지만, 정교한 시그니처(IDE 인텔리센스)까지 유지하려면 inspect.signature를 쓰거나 wrapt 같은 라이브러리를 고려.
인자 바인딩: wrapper는 보통 *args, **kwargs로 받아 원본에 그대로 전달한다. 특정 인자를 바꾸면 서프라이즈 발생.
평가 시점: 데코레이터는 함수 정의 시점에 실행된다. 모듈 import만 해도 동작할 수 있다는 뜻. 사이드이펙트는 신중히.
상태 보관: 카운터 같은 상태가 필요하면 nonlocal 변수나 functools.partial, 또는 클래스형 데코레이터를 쓴다.
from functools import wraps
def counted(func):
count = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"called {count} times")
return func(*args, **kwargs)
return wrapper
타입 힌트: 데코레이터는 타입 체커에 어려움을 준다. typing.Callable, TypeVar, ParamSpec, Concatenate를 적절히 쓰면 정적 분석 품질이 좋아진다.
from typing import Callable, TypeVar, ParamSpec
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def log_calls(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("calling", func.__name__)
return func(*args, **kwargs)
return wrapper
9) 마음속 모델(헷갈림 방지 요약)
함수는 1급 객체다. 즉, 숫자처럼 전달·반환할 수 있다. 데코레이터는 “함수를 입력으로 받아 함수를 출력하는 공장”이다. @는 그 공장을 짧게 부르는 표기다. wrapper가 원본 함수를 실행하기 전후로 무언가를 덧붙이거나, 결과를 바꾸거나, 실행 자체를 막을 수도 있다.
10) 손에 익히는 미니 연습문제 5개
-
time.sleep을 써서 어떤 함수의 실행 시간을 출력하는timeit데코레이터를 직접 작성. -
실패하면 최대 3번까지 재시도하는
retry(times=3, exceptions=(Exception,))데코레이터 작성. -
순수 함수에 한해 간단한 캐시를 붙이는
memoize데코레이터 작성(딕셔너리 키는 인자 튜플). -
비동기 함수용
async_retry버전 구현(await 필수). -
접근 제한: 특정 환경변수(
APP_ENV=prod)일 때만 함수를 실행하고 아니면 경고만 출력하는 데코레이터 작성.
원하는 패턴이 있으면 그대로 구현 예시를 더 만들어 줄게.