deep-dive2024-07-25·8 min·286/348

Python Decorator 파라미터 전달 기법

Python decorator에 파라미터를 전달하는 다양한 방법과 실전 활용 예제를 알아봅니다.

Python Decorator 파라미터 전달 기법

Python decorator는 함수나 클래스의 동작을 수정하는 강력한 도구입니다. decorator에 파라미터를 전달하는 방법을 이해하면 더 유연한 코드를 작성할 수 있습니다.

Environment

$ python --version
Python 3.11.5

$ python -c "import functools; print(functools.__name__)"
functools

Problem: decorator에 파라미터 전달 실패

def retry(func):
    def wrapper(*args, **kwargs):
        print(f"Retrying {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

# This works
@retry
def my_function():
    pass

# But this doesn't work as expected
@retry(max_retries=3)
def another_function():
    pass
# TypeError: retry() got an unexpected keyword argument 'max_retries'

Analysis: decorator 동작 원리

# 기본 decorator 구조
def simple_decorator(func):
    def wrapper(*args, **kwargs):
        # Before function call
        result = func(*args, **kwargs)
        # After function call
        return result
    return wrapper

# decorator with arguments - needs 3 levels of nesting
def decorator_with_args(arg1, arg2):
    def actual_decorator(func):
        def wrapper(*args, **kwargs):
            print(f"Args: {arg1}, {arg2}")
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

Solution: 다양한 decorator 파라미터 전달 방법

방법 1: 기본 decorator with arguments

import functools
import time

def retry(max_retries=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_retries - 1:
                        print(f"Attempt {attempt + 1} failed: {e}")
                        time.sleep(delay)
            
            raise last_exception
        return wrapper
    return decorator

# Usage
@retry(max_retries=5, delay=2)
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise ValueError("Random failure")
    return "Success!"

result = unreliable_function()

방법 2: class-based decorator

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)
    
    def reset_count(self):
        self.count = 0

@CountCalls
def say_hello():
    print("Hello!")

say_hello()  # Call 1 to say_hello
say_hello()  # Call 2 to say_hello
say_hello.reset_count()

방법 3: decorator factory with optional args

import functools

def log(level="INFO", include_args=True):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if include_args:
                args_str = str(args) if args else ""
                kwargs_str = str(kwargs) if kwargs else ""
                print(f"[{level}] {func.__name__}({args_str}, {kwargs_str})")
            else:
                print(f"[{level}] {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    
    # Support both @log and @log() syntax
    if callable(level):
        func = level
        level = "INFO"
        return decorator(func)
    
    return decorator

# Usage - both work
@log
def func1():
    pass

@log()
def func2():
    pass

@log(level="DEBUG", include_args=False)
def func3(x, y):
    return x + y

방법 4: decorator with class parameters

import functools
from typing import Callable, Any

class RateLimiter:
    def __init__(self, calls_per_second: float):
        self.calls_per_second = calls_per_second
        self.min_interval = 1.0 / calls_per_second
        self.last_call_time = 0
    
    def __call__(self, func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            import time
            current_time = time.time()
            time_since_last = current_time - self.last_call_time
            
            if time_since_last < self.min_interval:
                sleep_time = self.min_interval - time_since_last
                time.sleep(sleep_time)
            
            self.last_call_time = time.time()
            return func(*args, **kwargs)
        return wrapper

@RateLimiter(calls_per_second=10)
def api_call(endpoint):
    print(f"Calling {endpoint}")

방법 5: 메서드 decorator

import functools

def validate_types(*types):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Validate positional arguments
            for arg, expected_type in zip(args, types):
                if not isinstance(arg, expected_type):
                    raise TypeError(
                        f"Expected {expected_type}, got {type(arg)}"
                    )
            return func(*args, **kwargs)
        return wrapper
    return decorator

class Calculator:
    @validate_types(int, int)
    def add(self, a, b):
        return a + b
    
    @validate_types(float, float)
    def multiply(self, a, b):
        return a * b

calc = Calculator()
print(calc.add(5, 3))      # 8
print(calc.multiply(2.5, 4.0))  # 10.0

try:
    calc.add("5", 3)
except TypeError as e:
    print(f"Error: {e}")

Lessons Learned

  1. functools.wraps 필수: decorator를 작성할 때 반드시 @functools.wraps(func)를 사용하여 원본 함수의 메타데이터를 보존하세요.

  2. 3중 중첩: decorator에 인자가 필요하면 decorator → decorator factory → wrapper의 3중 중첩 구조를 사용합니다.

  3. 호환성 고려: @decorator@decorator() 두 문법 모두 지원하도록 설계하면 더 유연합니다.

  4. 클래스 기반 decorator: 복잡한 상태 관리가 필요하면 클래스 기반 decorator를 사용하는 것이 더 깔끔합니다.

  5. 타입 힌트: decorator의 파라미터와 반환값에 타입 힌트를 추가하면 IDE 지원이 향상됩니다.


This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.