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

함수가 함수를 감싸기 — Decorator 의 정체

~22 min · decorator, wrapper, first-class, @

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

@ 가 진짜 하는 일

decorator 는 함수 받아서 함수 반환하는 함수. 정의 위의 @decorator 문법은 단지 단축형 — @deco + def f(): ...def f(): ... 다음 f = deco(f) 와 정확히 같음. 그게 전부. 이거 보면 모든 decorator 가 마법 아니게 돼.

감싸기 패턴 — 정석 모양

다른 함수를 입력으로 받는 함수 (decorator) 정의. 안에서 wrapper 함수 정의 — 원래 함수 호출, 호출 전/후/주변에 추가 로직 가능. wrapper 반환. 사용자가 @deco 쓰면 wrapper 가 원래 이름에 바인딩.

왜 wrapper 에 *args / **kwargs 필요한가

decorator 는 감싸는 함수가 어떤 인자 받는지 몰라. wrapper 가 def wrapper(): 로 하드코딩하면 0 인자 함수에서만 작동. idiomatic 모양 — def wrapper(*args, **kwargs): return fn(*args, **kwargs). wrapper 가 모든 거 받고 모든 거 forward. 추가 로직은 호출 전/후에.

decorator 가 좋은 일

cross-cutting concerns. 모든 API 호출 주변에 로깅. 시간 측정. 비싼 계산 캐싱. 인증 체크. 재시도 로직. 함수 본문 안이 아니라 개념적으로 함수를 감싸는 모든 거. 패턴 — "함수 자체를 안 건드리고 모든 호출 경계에 동작 추가".

원칙: decorator = 합성. 함수는 값, decorator 는 한 값을 다른 값으로 변환. 언어에 특별한 "decorator 메커니즘" 없어 — 단지 함수 받아 함수 반환하는 함수 + sugar 문법. 나머지 다 이 한 가지 위에.

스택 — 한 함수에 여러 decorator

decorator 는 아래에서 위로 적용. @a + @b + def f():f = a(b(f)). 함수에 가장 가까운 decorator 가 먼저. 순서가 중요할 때 (logging-then-cache vs cache-then-logging 다른 결과) 중요.

Code

@deco 는 f = deco(f) 의 sugar·python
def announce(fn):
    def wrapper():
        print("calling", fn.__name__)
        result = fn()
        print("finished")
        return result
    return wrapper

# 둘은 *정확히* 같음:

# 형태 1 — 수동
def hello():
    print("hi")
hello = announce(hello)

# 형태 2 — sugar
@announce
def hello():
    print("hi")

hello()
# calling hello
# hi
# finished
모든 거 forward 하는 wrapper 패턴·python
import time

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

@timed
def slow_add(a, b):
    time.sleep(0.1)
    return a + b

print(slow_add(3, 4))
# slow_add took ~100ms
# 7

# 어떤 시그니처와도 작동 — *args/**kwargs forwarding 덕분
@timed
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}"

print(greet("Pippa", greeting="Hi"))
로깅 — 가장 흔한 실제 용도·python
def log_calls(fn):
    def wrapper(*args, **kwargs):
        arglist = ", ".join(
            [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
        )
        print(f"call: {fn.__name__}({arglist})")
        result = fn(*args, **kwargs)
        print(f"  -> {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 4)
# call: add(3, 4)
#   -> 7

add(b=10, a=2)
# call: add(a=2, b=10)
#   -> 12
decorator 스택 — 아래에서 위로·python
def shout(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper

def exclaim(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs) + "!"
    return wrapper

# @shout 마지막 적용 — 호출 사이트에 가까움
# @exclaim 처음 적용 — 함수에 가까움
@shout
@exclaim
def greet(name):
    return f"hello {name}"

print(greet("pippa"))   # 'HELLO PIPPA!'

# greet = shout(exclaim(greet)) 와 같음
# 1. exclaim 이 greet 감싸 '!' 추가
# 2. shout 이 그걸 감싸 대문자로

External links

Exercise

decorator count_calls(fn) 작성 — fn 호출 + 값 반환 외에 wrapper 함수가 호출된 횟수 카운트. count 는 wrapped_fn.calls 로 접근 가능. 다른 두 함수에 적용해서 각자 독립 카운트 갖는지 확인. (힌트 — wrapper 자체에 count 저장: wrapper.calls = 0 후 증가.)

Progress

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

댓글 0

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

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