// education

데코레이터와 메타프로그래밍

들어가며

데코레이터는 함수나 클래스의 동작을 수정하거나 확장하는 강력한 도구입니다. 메타프로그래밍을 통해 런타임에 프로그램의 구조를 동적으로 변경할 수 있습니다.

데코레이터의 기본

예제 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의 강력한 기능입니다. 이를 이용하면 코드를 더욱 모듈화하고 재사용성을 높일 수 있습니다.

← 이전파일 I/O와 데이터 처리 다음 →제너레이터와 효율적 코딩