본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

상태가 자라면 클래스로 데코레이트하기

~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

인스턴스도 호출할 수 있어

클래스의 __init__에서 꾸밀 함수를 받고 __call__에서 실제 호출을 처리하면 그 인스턴스가 데코레이터이자 래퍼가 돼. 호출 횟수, 캐시, 통계, 초기화 메서드처럼 여러 상태와 행동을 한 객체에 모을 수 있어.

메타데이터는 직접 보존해

함수형 래퍼의 wraps 대신 functools.update_wrapper(self, fn)로 인스턴스에 원래 함수 정보를 옮겨. 설정을 받는 클래스 데코레이터라면 생성 단계와 함수 결합 단계도 분리해야 해.

작은 상태라면 클로저가 더 가볍다

카운터 하나 때문에 클래스의 수명과 인터페이스를 만들 필요는 없어. 상태를 조회·초기화하거나 여러 메서드가 함께 지킬 규칙이 생길 때 클래스로 올려.

Code

__init__과 __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 자체에
설정을 받는 클래스 데코레이터·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
함수형 데코레이터와 함께 쌓기·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
상태가 작을 때는 클로저로 충분하다·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

순수 함수의 결과를 인자별로 저장하는 클래스 데코레이터 Memoize를 만들어. cache_clear()와 hit·miss·size를 돌려주는 cache_info()를 제공하고 update_wrapper로 함수 정보를 보존해. 재귀 피보나치에 붙여 fib(50)이 빠르게 끝나는지 확인해.

Progress

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

댓글 0

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

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