데코레이터 (Decorator)
학습 목표
데코레이터의 동작 원리를 이해한다
@wraps를 사용하여 메타데이터를 보존할 수 있다
매개변수가 있는 데코레이터를 작성할 수 있다
실무에서 자주 사용하는 데코레이터 패턴을 활용할 수 있다
왜 중요한가
데코레이터는 기존 함수의 동작을 수정하지 않고 확장하는 패턴입니다. Python 생태계 전반에서 사용됩니다: @property, @staticmethod, @torch.no_grad(), @app.route(), @pytest.fixture 등. 데코레이터를 이해하면 프레임워크 코드를 읽고, 자신만의 확장을 작성할 수 있습니다.
데코레이터 원리
데코레이터는 함수를 인자로 받아 새 함수를 반환하는 함수입니다.
# 데코레이터 정의
def my_decorator ( func ):
def wrapper ( * args , ** kwargs ):
print ( "함수 실행 전" )
result = func( * args, ** kwargs)
print ( "함수 실행 후" )
return result
return wrapper
# @ 문법으로 적용
@my_decorator
def greet ( name ):
print ( f "안녕하세요, { name } !" )
greet( "김철수" )
# 출력:
# 함수 실행 전
# 안녕하세요, 김철수!
# 함수 실행 후
# 위의 @my_decorator는 아래와 동일
# greet = my_decorator(greet)
@wraps로 메타데이터 보존
from functools import wraps
def my_decorator ( func ):
@wraps (func) # 원본 함수의 이름, docstring 보존
def wrapper ( * args , ** kwargs ):
return func( * args, ** kwargs)
return wrapper
@my_decorator
def greet ( name ):
"""인사 메시지를 출력합니다."""
print ( f "안녕하세요, { name } !" )
# @wraps 없으면
print (greet. __name__ ) # "greet" (@wraps 없으면 "wrapper")
print (greet. __doc__ ) # "인사 메시지를 출력합니다."
@wraps(func)를 항상 추가하세요. 없으면 디버깅, 문서화, 테스트에서 문제가 발생합니다.
실용 데코레이터 패턴
실행 시간 측정
import time
from functools import wraps
def timer ( func ):
@wraps (func)
def wrapper ( * args , ** kwargs ):
start = time.perf_counter()
result = func( * args, ** kwargs)
elapsed = time.perf_counter() - start
print ( f " { func. __name__ } : { elapsed :.4f} 초" )
return result
return wrapper
@timer
def slow_function ():
time.sleep( 1 )
return "완료"
slow_function() # slow_function: 1.0012초
재시도 (Retry)
def retry ( max_attempts = 3 , delay = 1 ):
def decorator ( func ):
@wraps (func)
def wrapper ( * args , ** kwargs ):
for attempt in range ( 1 , max_attempts + 1 ):
try :
return func( * args, ** kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print ( f "시도 { attempt } 실패: { e } . { delay } 초 후 재시도..." )
time.sleep(delay)
return wrapper
return decorator
@retry ( max_attempts = 3 , delay = 2 )
def fetch_data ( url ):
"""데이터를 가져옵니다."""
# 네트워크 요청...
pass
def cache ( func ):
"""결과를 캐싱하는 데코레이터"""
memo = {}
@wraps (func)
def wrapper ( * args ):
if args not in memo:
memo[args] = func( * args)
return memo[args]
return wrapper
@cache
def fibonacci ( n ):
if n < 2 :
return n
return fibonacci(n - 1 ) + fibonacci(n - 2 )
print (fibonacci( 100 )) # 즉시 계산 (캐싱 덕분)
Python 내장 functools.lru_cache가 더 강력한 캐싱을 제공합니다. from functools import lru_cache
@lru_cache ( maxsize = 128 )
def fibonacci ( n ):
if n < 2 :
return n
return fibonacci(n - 1 ) + fibonacci(n - 2 )
매개변수 있는 데코레이터
# 3단 구조: 팩토리 -> 데코레이터 -> 래퍼
def repeat ( n ):
"""함수를 n번 반복 실행합니다."""
def decorator ( func ):
@wraps (func)
def wrapper ( * args , ** kwargs ):
results = []
for _ in range (n):
results.append(func( * args, ** kwargs))
return results
return wrapper
return decorator
@repeat ( 3 )
def say_hello ():
print ( "Hello!" )
return "done"
say_hello()
# Hello!
# Hello!
# Hello!
데코레이터 스택
여러 데코레이터를 중첩할 수 있습니다. 아래에서 위로 적용됩니다.
@timer
@retry ( max_attempts = 3 )
def process_data ():
pass
# 실행 순서: timer(retry(process_data))
# timer가 가장 바깥, retry가 안쪽
AI/ML에서의 활용
import torch
from functools import wraps
# PyTorch @torch.no_grad() 유사 패턴
def no_grad ( func ):
"""그래디언트 계산을 비활성화합니다."""
@wraps (func)
def wrapper ( * args , ** kwargs ):
with torch.no_grad():
return func( * args, ** kwargs)
return wrapper
@no_grad
def evaluate ( model , data ):
return model(data)
# 로깅 데코레이터
def log_experiment ( func ):
@wraps (func)
def wrapper ( * args , ** kwargs ):
print ( f "실험 시작: { func. __name__ } " )
print ( f "매개변수: args= { args } , kwargs= { kwargs } " )
result = func( * args, ** kwargs)
print ( f "실험 완료: { result } " )
return result
return wrapper
@log_experiment
def train_model ( model_name , epochs = 10 , lr = 0.001 ):
# 학습 로직...
return { "accuracy" : 0.95 }
# 입력 검증 데코레이터
def validate_inputs ( ** validators ):
def decorator ( func ):
@wraps (func)
def wrapper ( ** kwargs ):
for param, check in validators.items():
if param in kwargs and not check(kwargs[param]):
raise ValueError ( f "유효하지 않은 { param } : { kwargs[param] } " )
return func( ** kwargs)
return wrapper
return decorator
@validate_inputs (
lr = lambda x : 0 < x < 1 ,
epochs = lambda x : x > 0 ,
batch_size = lambda x : x > 0 and (x & (x - 1 ) == 0 ) # 2의 거듭제곱
)
def train ( lr = 0.001 , epochs = 10 , batch_size = 32 ):
pass
네. __call__ 메서드를 구현한 클래스는 데코레이터로 사용할 수 있습니다. 상태가 복잡할 때 클래스 기반 데코레이터가 유용합니다.
네. @property는 메서드를 속성처럼 접근할 수 있게 만드는 내장 데코레이터입니다. OOP 문서에서 자세히 다룹니다.
체크리스트
다음 문서