들어가며
데코레이터는 함수나 클래스의 동작을 수정하거나 확장하는 강력한 도구입니다. 메타프로그래밍을 통해 런타임에 프로그램의 구조를 동적으로 변경할 수 있습니다.
데코레이터의 기본
예제 1: 함수 데코레이터
def my_decorator(func):
"""함수를 감싸는 데코레이터"""
def wrapper():
print("함수 실행 전")
func()
print("함수 실행 후")
return wrapper
# 데코레이터 적용
@my_decorator
def greet():
print("안녕하세요!")
greet()
출력:
함수 실행 전
안녕하세요!
함수 실행 후
예제 2: 인자를 받는 데코레이터
def repeat(times):
"""n번 반복하는 데코레이터"""
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hello(name):
print(f"안녕하세요, {name}!")
say_hello("Alice")
출력:
안녕하세요, Alice!
안녕하세요, Alice!
안녕하세요, Alice!
예제 3: 함수 인자 처리 데코레이터
import functools
import time
def timing(func):
"""함수 실행 시간을 측정하는 데코레이터"""
@functools.wraps(func) # 원본 함수의 메타데이터 보존
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 실행 시간: {end - start:.4f}초")
return result
return wrapper
@timing
def slow_function():
time.sleep(0.5)
return "완료"
result = slow_function()
print(f"결과: {result}")
출력:
slow_function 실행 시간: 0.5012초
결과: 완료
클래스 데코레이터
예제 4: 클래스 데코레이터
def validate_init(cls):
"""클래스의 __init__을 검증하는 데코레이터"""
original_init = cls.__init__
def new_init(self, *args, **kwargs):
print(f"{cls.__name__} 인스턴스 생성 중...")
original_init(self, *args, **kwargs)
print(f"{cls.__name__} 인스턴스 생성 완료")
cls.__init__ = new_init
return cls
@validate_init
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("Alice", 25)
출력:
Person 인스턴스 생성 중...
Person 인스턴스 생성 완료
고급 데코레이터
예제 5: 캐싱 데코레이터
import functools
def memoize(func):
"""결과를 캐싱하는 데코레이터"""
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
print(f"캐시 미스: {args}")
else:
print(f"캐시 히트: {args}")
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(f"fib(5) = {fibonacci(5)}")
print(f"fib(5) = {fibonacci(5)}") # 캐시 히트
출력:
캐시 미스: (5,)
캐시 미스: (4,)
...
캐시 히트: (5,)
fib(5) = 5
예제 6: 로깅 데코레이터
import functools
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def log_calls(func):
"""함수 호출을 로깅하는 데코레이터"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger.info(f"호출: {func.__name__}({args}, {kwargs})")
try:
result = func(*args, **kwargs)
logger.info(f"반환: {result}")
return result
except Exception as e:
logger.error(f"예외: {e}")
raise
return wrapper
@log_calls
def divide(a, b):
return a / b
divide(10, 2)
# divide(10, 0) # 예외 발생
출력:
INFO:__main__:호출: divide((10, 2), {})
INFO:__main__:반환: 5.0
메타프로그래밍
예제 7: 동적 속성 추가
def add_method(cls, name, func):
"""클래스에 메서드를 동적으로 추가"""
setattr(cls, name, func)
return cls
class MyClass:
pass
# 동적으로 메서드 추가
def new_method(self):
return "동적으로 추가된 메서드"
add_method(MyClass, "dynamic_method", new_method)
obj = MyClass()
print(obj.dynamic_method())
출력:
동적으로 추가된 메서드
예제 8: getattr 사용
class DynamicObject:
"""동적으로 속성을 처리하는 객체"""
def __init__(self):
self._data = {}
def __getattr__(self, name):
"""존재하지 않는 속성 접근 시"""
if name in self._data:
return self._data[name]
return f"'{name}' 속성이 없습니다"
def __setattr__(self, name, value):
"""속성 설정 시"""
if name.startswith('_'):
super().__setattr__(name, value)
else:
self._data[name] = value
obj = DynamicObject()
obj.name = "Alice"
obj.age = 25
print(f"name: {obj.name}")
print(f"age: {obj.age}")
print(f"email: {obj.email}")
출력:
name: Alice
age: 25
email: 'email' 속성이 없습니다
예제 9: 메타클래스를 사용한 추적
class TrackedMeta(type):
"""모든 인스턴스 생성을 추적하는 메타클래스"""
def __init__(cls, name, bases, dct):
super().__init__(name, bases, dct)
cls._instances = []
def __call__(cls, *args, **kwargs):
instance = super().__call__(*args, **kwargs)
cls._instances.append(instance)
return instance
class TrackedClass(metaclass=TrackedMeta):
def __init__(self, value):
self.value = value
# 인스턴스 생성
obj1 = TrackedClass(1)
obj2 = TrackedClass(2)
obj3 = TrackedClass(3)
print(f"생성된 인스턴스 수: {len(TrackedClass._instances)}")
print(f"인스턴스 값들: {[obj.value for obj in TrackedClass._instances]}")
출력:
생성된 인스턴스 수: 3
인스턴스 값들: [1, 2, 3]
흔한 실수와 해결책
실수 1: functools.wraps 누락
틀린 예제:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""원본 함수의 문서"""
pass
print(my_function.__doc__) # None - 문서가 손실됨!
올바른 예제:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before")
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""원본 함수의 문서"""
pass
print(my_function.__doc__) # 원본 함수의 문서
실수 2: 클로저 변수 공유
틀린 예제:
decorators = []
for i in range(3):
def decorator(func):
def wrapper():
print(f"i = {i}")
return func()
return wrapper
decorators.append(decorator)
# 모두 i = 2를 출력 (클로저가 같은 i를 참조)
올바른 예제:
decorators = []
for i in range(3):
def make_decorator(value):
def decorator(func):
def wrapper():
print(f"i = {value}")
return func()
return wrapper
return decorator
decorators.append(make_decorator(i))
연습 문제
문제 1: 권한 검증 데코레이터
사용자의 권한을 확인하는 데코레이터를 작성하세요.
문제 2: 재시도 데코레이터
실패한 함수를 자동으로 재시도하는 데코레이터를 작성하세요.
문제 3: 입력 검증 데코레이터
함수 인자의 타입을 검증하는 데코레이터를 작성하세요.
풀이
문제 1 풀이
import functools
def require_permission(permission):
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if hasattr(user, 'permissions') and permission in user.permissions:
return func(user, *args, **kwargs)
else:
raise PermissionError(f"'{permission}' 권한이 필요합니다")
return wrapper
return decorator
class User:
def __init__(self, name, permissions):
self.name = name
self.permissions = permissions
@require_permission("admin")
def delete_user(user, target):
return f"{user.name}이 {target}을 삭제했습니다"
admin = User("Alice", ["admin", "edit"])
regular = User("Bob", ["edit"])
print(delete_user(admin, "user@example.com"))
# print(delete_user(regular, "user@example.com")) # PermissionError
문제 2 풀이
import functools
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"시도 {attempt + 1} 실패, {delay}초 후 재시도...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=1)
def unreliable_function():
import random
if random.random() < 0.7:
raise Exception("실패")
return "성공"
result = unreliable_function()
print(result)
문제 3 풀이
import functools
def validate_types(**types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for key, expected_type in types.items():
if key in kwargs:
if not isinstance(kwargs[key], expected_type):
raise TypeError(
f"{key}은(는) {expected_type.__name__} 타입이어야 합니다"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int, height=float)
def create_person(name, age, height):
return f"{name}, {age}세, {height}cm"
print(create_person(name="Alice", age=25, height=175.5))
# create_person(name="Bob", age="30", height=180.0) # TypeError
마무리
데코레이터와 메타프로그래밍은 Python의 강력한 기능입니다. 이를 이용하면 코드를 더욱 모듈화하고 재사용성을 높일 수 있습니다.