파이썬 데코레이터 — 함수를 감싸는 함수
원본을 안 건드리고 능력만 추가. @flask.route·@dataclass 의 비밀.
웹 프레임워크 코드를 보면 @app.route("/"), 14편의 @dataclass, 디버깅용 @functools.lru_cache 같은 골뱅이 표기가 함수 위에 자주 붙어있습니다. 이게 데코레이터(decorator). 본래 함수에 손대지 않고 "이 함수에 능력 X 를 추가하라" 라는 선언입니다. 17편부터 중급 묶음 — 입문·기초가 끝나고 라이브러리 소스 코드가 읽히기 시작하는 지점.
17편을 마치면 ① 함수가 일급 객체라는 의미 ② @decorator 문법의 정체 ③ *args/**kwargs 로 어떤 함수든 감싸기 ④ functools.wraps 의 이유 ⑤ 표준 라이브러리 단골 데코레이터 — 5가지가 손에 익습니다.
함수가 일급 객체라는 말
파이썬에서 함수도 값입니다. 변수에 담을 수 있고, 다른 함수의 인자로 넘길 수 있고, 함수가 함수를 돌려줄 수도 있어요.
def square(n):
return n * n
f = square # 함수를 변수에 — 호출 아님(괄호 없음)
f(5) # 25 — f 가 square 와 같은 함수
type(f) # <class 'function'>
# 함수를 인자로
def apply(fn, x):
return fn(x)
apply(square, 7) # 49
# 함수가 함수를 반환
def make_multiplier(factor):
def inner(x):
return x * factor
return inner
double = make_multiplier(2)
triple = make_multiplier(3)
double(5), triple(5) # (10, 15)
마지막 패턴이 데코레이터의 뼈대. make_multiplier 가 자기 안에 inner 함수를 만들고 그걸 돌려줍니다. inner 는 factor 를 기억해요 — 이걸 클로저(closure) 라 부르고, 18편 타입힌트와도 연결됩니다.
@decorator — 함수를 감싸는 함수
import time
def timer(fn):
def wrapped(*args, **kwargs):
t0 = time.time()
result = fn(*args, **kwargs)
elapsed = time.time() - t0
print(f"{fn.__name__}: {elapsed:.3f} 초")
return result
return wrapped
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000)
# slow_sum: 0.187 초
# 49999995000000
@timer 한 줄이 이 코드와 같습니다:
def slow_sum(n):
return sum(range(n))
slow_sum = timer(slow_sum) # 원본 함수를 timer 로 감싸 다시 할당
핵심 패턴 세 가지.
- 데코레이터는 함수를 받아 함수를 돌려준다 — timer 가 fn 을 받고 wrapped 를 반환
- wrapped 안에서 원본 호출 —
result = fn(*args, **kwargs). 앞뒤로 부가 동작 *args, **kwargs로 모든 시그니처 수용 — 어떤 인자 조합이든 받아 그대로 전달
*args 와 **kwargs — 가변 인자
def f(*args, **kwargs):
print(args) # 위치 인자들이 튜플로
print(kwargs) # 키워드 인자들이 딕셔너리로
f(1, 2, 3, name="준성", age=30)
# (1, 2, 3)
# {'name': '준성', 'age': 30}
# 펼쳐서 전달
nums = [1, 2, 3]
print(*nums) # 1 2 3 — 리스트를 위치 인자로 펼침
config = {"sep": "-", "end": "!\n"}
print("a", "b", "c", **config) # a-b-c!
데코레이터에서 *args, **kwargs 가 거의 표준 — "원본 함수가 어떤 시그니처든 그대로 전달" 한다는 선언이에요.
functools.wraps — 자기 정체성을 잃지 않기
위의 @timer 에는 미묘한 버그가 있습니다. 감싼 뒤 함수의 이름·docstring 이 사라져요.
@timer
def slow_sum(n):
"""0부터 n-1 까지의 합."""
return sum(range(n))
slow_sum.__name__ # 'wrapped' ❌ 'slow_sum' 이어야
slow_sum.__doc__ # None ❌ docstring 사라짐
해결은 functools.wraps 데코레이터(데코레이터를 만드는 데코레이터). 한 줄만 추가.
from functools import wraps
def timer(fn):
@wraps(fn) # ← 이 한 줄로 메타데이터 보존
def wrapped(*args, **kwargs):
...
return wrapped
slow_sum.__name__ # 'slow_sum' ✓
slow_sum.__doc__ # '0부터 n-1 까지의 합.' ✓
데코레이터를 만들 땐 거의 항상 @wraps 를 같이. 디버깅·문서 자동화 때 본인을 구합니다.
표준 라이브러리 단골 데코레이터 + 마무리
📌 자주 만나는 데코레이터 5종
· @functools.lru_cache — 인자가 같으면 결과를 캐싱. 재귀 피보나치 100배 빠르게
· @functools.cache — 무제한 캐시 (3.9+)
· @dataclass — 14편의 자동 클래스 생성
· @property — 메서드를 속성처럼 호출하게
· @staticmethod·@classmethod — self 안 받는 메서드
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
fib(100) # 354224848179261915075 — 캐시 없으면 우주가 끝남
다음 미션: ① @logger 데코레이터 — 호출 시작·끝·인자·반환값 출력 ② @lru_cache 로 피보나치 속도 비교 ③ @retry(times=3) 처럼 인자 받는 데코레이터 도전 (힌트: 한 단계 더 감싸기).
다음 편 미리보기
18편 — "파이썬 타입 힌트": def f(x: int) -> str. 동적 타입의 부드러운 안전망. mypy·VS Code 활용.
← 16편 "venv" · 다음: 18편 "타입 힌트"