C.W.K.
Stream
Lesson 04 of 05 · published

클래스 decorator + __call__ — wrapper 에 state 필요할 때

~18 min · class-decorator, __call__, stateful-wrapper

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

두 가지 다른 "클래스 decorator"

헷갈리는 용어 — "클래스 decorator" 가 (a) 클래스로 *구현* 된 decorator 또는 (b) 클래스에 *적용* 된 decorator 일 수 있음. 여기선 (a) 얘기. (b) 는 "마침 클래스에 적용된 decorator" — 다른 decorator 와 같은 룰.

왜 함수 대신 클래스

함수 기반 decorator 는 closure 변수에 state 저장. 클래스 기반은 속성에 (self.count, self.config). state 가 충분히 복잡해서 다루는 메서드 (reset, stats 등) 가 필요하면 클래스. 단순 wrapper 는 함수 OK.

__call__ 이 인스턴스를 호출 가능하게

클래스에 __call__ 정의하면 인스턴스가 함수처럼 동작. my_instance(args)my_instance.__call__(args) 호출. 클래스-as-decorator 는 nested 함수 대신 __call__ 안에서 wrapper 일을 함.

모양 — __init__ 이 함수 받고, __call__ 이 일 함

클래스의 __init__ 이 wrapped 함수 저장. __call__*args, **kwargs 받고 wrapper 로직 + 저장된 함수 호출. 필요하면 다른 메서드 (reset, stats) 추가.

원칙: 함수 기반 decorator 가 디폴트 — 다음이 필요하면 클래스 — decorator 객체에 메서드 여러 개, 구조 있는 복잡한 내부 state, decorator 간 상속/합성. 함수 형태가 어색할 때만 클래스.

Code

클래스를 decorator 로 — __init__ 이 fn 받고, __call__ 이 감쌈·python
import functools

class CountCalls:
    def __init__(self, fn):
        self.fn = fn
        self.count = 0
        functools.update_wrapper(self, fn)   # 클래스용 @wraps

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

    def reset(self):
        self.count = 0

@CountCalls
def hello():
    return "hi"

hello()
hello()
hello()
print(hello.count)         # 3
hello.reset()
print(hello.count)         # 0

# 일반 함수 decorator 에 없는 메서드 사용 가능
# .reset(), .count 가 wrapper 자체에
인자 받는 클래스 decorator·python
import functools

class RateLimit:
    def __init__(self, max_per_minute):
        self.max = max_per_minute
        self.calls = []

    def __call__(self, fn):                # 함수에 적용 시 호출
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            import time
            now = time.time()
            # 60 초 넘은 호출 떨어뜨림
            self.calls = [t for t in self.calls if now - t < 60]
            if len(self.calls) >= self.max:
                raise RuntimeError("rate limited")
            self.calls.append(now)
            return fn(*args, **kwargs)
        return wrapper

@RateLimit(max_per_minute=5)
def do_work():
    return "ok"

for _ in range(5):
    print(do_work())   # 다 'ok'
# 6 번째는 raise
클래스와 함수 decorator 합성·python
import functools
import time

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

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

def timed(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__}: {(time.perf_counter()-start)*1000:.2f}ms")
        return result
    return wrapper

@CountCalls
@timed                   # 처음 적용 — 원래 거 감쌈
def compute(n):
    return sum(range(n))

compute(1000)
compute(2000)
print(compute.count)     # 2
클래스 안 쓸 때 — 단순 state·python
import functools

# 함수 형태 — 단순 state 엔 완벽
def counter(fn):
    count = 0
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        wrapper.calls = count    # wrapper 자체에 노출
        return fn(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@counter
def ping():
    return "pong"

ping(); ping(); ping()
print(ping.calls)   # 3

# 이 경우엔 함수 형태가 더 짧고 명확.
# 메서드 여러 개나 풍부한 state 필요할 때만 클래스.

External links

Exercise

클래스 기반 decorator Memoize 구현 — 순수 함수 결과 캐시. 클래스 — (a) 인자 키 기반 dict 에 결과 저장, (b) cache_clear() 메서드, (c) cache_info() 메서드 (hit / miss / size 반환), (d) functools.update_wrapper 로 메타데이터 보존. 재귀 피보나치 함수에 적용 — decorator 와 함께면 fib(50) 도 빠르다는 거 보여.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.