본문으로 건너뛰기
김신건의 로그

[Python] 데코레이터: @decorator, functools.wraps, 클래스 데코레이터

· 수정 · 📖 약 1분 · 490자/단어 #python #decorator #function #advanced
python decorator, @decorator, functools.wraps, decorator with args, class decorator

정의

**데코레이터(decorator)**는 함수/클래스를 받아 다른 함수/클래스를 반환하는 callable이다. @decorator 문법은 단순한 호출 변환에 불과:

@my_decorator
def f(): pass

# 동등
def f(): pass
f = my_decorator(f)

기본 데코레이터

import time

def timing(fn):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__}: {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

@timing
def slow_add(a, b):
    time.sleep(0.5)
    return a + b

print(slow_add(1, 2))

wrapper는 원래 함수의 행동에 시간 측정을 추가한 새 함수.

functools.wraps: 메타데이터 보존

위의 wrapper__name__, __doc__, __module__, __wrapped__ 등이 원본과 달라진다. 디버깅·문서화 도구가 망가짐.

python
from functools import wraps

def timing(fn):
  @wraps(fn)
  def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
  return wrapper

@timing
def add(a, b):
  """두 수 더하기."""
  return a + b

print(add.__name__)
print(add.__doc__)
print(add.__wrapped__)
결과
add
두 수 더하기.
<function add at 0x...>

모든 데코레이터에 @wraps(fn) 붙이는 게 표준.

인수 받는 데코레이터

데코레이터에 인수가 필요하면 3단 함수가 된다.

from functools import wraps

def retry(times=3):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            for i in range(times):
                try:
                    return fn(*args, **kwargs)
                except Exception as e:
                    if i == times - 1:
                        raise
                    print(f"retry {i + 1}: {e}")
        return wrapper
    return decorator

@retry(times=5)
def flaky():
    ...
  • retry(times=5)decorator 반환
  • @decoratorwrapper 반환

인수 없이도 호출 가능한 데코레이터

def my_decorator(fn=None, *, prefix="LOG"):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            print(prefix, fn.__name__)
            return fn(*args, **kwargs)
        return wrapper
    return decorator if fn is None else decorator(fn)

@my_decorator                  # 인수 없이
def a(): pass

@my_decorator(prefix="DEBUG")  # 인수 있게
def b(): pass

여러 데코레이터 체이닝

@decorator_a
@decorator_b
@decorator_c
def f(): ...

# 동등
f = decorator_a(decorator_b(decorator_c(f)))

아래에서 위로 적용. 위 예시는 c → b → a 순서.

흔한 표준 라이브러리 데코레이터

functools.lru_cache

from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    if n < 2: return n
    return fib(n - 1) + fib(n - 2)

fib(100)        # 빠르게
fib.cache_info()  # 캐시 통계
fib.cache_clear()

@cache (3.9+)는 unbounded lru_cache(maxsize=None).

functools.cached_property

from functools import cached_property

class DataSet:
    def __init__(self, data):
        self.data = data

    @cached_property
    def mean(self):
        print("computing")
        return sum(self.data) / len(self.data)

d = DataSet([1, 2, 3])
d.mean        # "computing" 출력 후 2.0
d.mean        # 캐시 사용, 출력 없음

인스턴스 속성으로 저장되므로 무효화는 del d.mean.

contextlib.contextmanager

함수 하나로 컨텍스트 매니저 생성.

from contextlib import contextmanager

@contextmanager
def timer(label):
    import time
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.4f}s")

with timer("work"):
    do_work()

클래스 데코레이터

함수 대신 클래스에 적용. 클래스를 수정·교체하는 데 사용.

def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

print(User("Alice", 30))    # User(name='Alice', age=30)

@dataclass도 본질적으로 이런 클래스 데코레이터.

인스턴스 호출 가능 객체로서의 데코레이터

__call__을 구현한 클래스도 데코레이터로 쓸 수 있다.

class CountCalls:
    def __init__(self, fn):
        wraps(fn)(self)
        self.fn = fn
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.fn(*args, **kwargs)

@CountCalls
def greet():
    print("hello")

greet(); greet(); greet()
print(greet.count)    # 3

상태가 필요한 데코레이터에 유용.

메서드 데코레이터

인스턴스 메서드에도 적용 가능. self가 첫 인수.

def log_method(fn):
    @wraps(fn)
    def wrapper(self, *args, **kwargs):
        print(f"{type(self).__name__}.{fn.__name__}({args}, {kwargs})")
        return fn(self, *args, **kwargs)
    return wrapper

class API:
    @log_method
    def get(self, url): ...

함정과 팁

  • @wraps 빠뜨리지 말 것: stacktrace, help() 망가짐
  • 데코레이터는 import 시점에 적용: 부작용 주의
  • 인수 받는 데코레이터의 시그니처 일관성: @d@d() 둘 다 지원하면 사용자가 헷갈림
  • 타입 힌트 보존: ParamSpec (3.10+) 활용하면 mypy/pyright가 wrapper 시그니처 추적 가능
from typing import Callable, ParamSpec, TypeVar
from functools import wraps

P = ParamSpec("P")
R = TypeVar("R")

def my_decorator(fn: Callable[P, R]) -> Callable[P, R]:
    @wraps(fn)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        return fn(*args, **kwargs)
    return wrapper

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기