← 파이썬 강의 목록으로
🚀
고급 (Advanced)
@deco · functools.wraps · with · contextmanager

2주차 — 데코레이터와 컨텍스트 매니저

함수 데코레이터, 인자 받는 데코레이터, 클래스/함수 기반 컨텍스트 매니저까지 — 횡단 관심사를 깔끔히 분리하는 두 도구를 익힙니다.

decoratorwrapswithcontextmanager
소요 시간
2시간
난이도
📊 고급
선수 조건
🎯 고급 1주차
결과물
데코레이터·컨텍스트 매니저 직접 구현

이 강의에서 배우는 것

  • 1함수 데코레이터를 정의하고 적용한다
  • 2인자 받는 데코레이터를 만든다
  • 3클래스/함수 기반 컨텍스트 매니저를 구현한다
  • 4@functools.wraps 의 역할을 안다

1. 데코레이터란

함수를 인자로 받아 새로운 함수를 반환합니다. 로깅·캐싱·인증 같은 기능을 횡단으로 추가.

python
def loud(func):
    def wrapper(*args, **kwargs):
        print(f">>> {func.__name__} 호출")
        result = func(*args, **kwargs)
        print(f"<<< 결과: {result}")
        return result
    return wrapper

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

add(3, 5)
# >>> add 호출
# <<< 결과: 8

@loud 는 add = loud(add) 의 문법 설탕입니다.

2. functools.wraps

데코레이터로 감싸도 원본 함수 이름·docstring 을 유지합니다.

python
from functools import wraps

def loud(func):
    @wraps(func)            # 이거 없으면 .__name__ 이 'wrapper' 됨
    def wrapper(*args, **kwargs):
        ...
    return wrapper

3. 인자 받는 데코레이터

데코레이터를 만드는 함수 — 3중 함수 구조.

python
def retry(times):
    def deco(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"재시도 {i+1}/{times}: {e}")
            raise
        return wrapper
    return deco

@retry(3)
def fragile():
    ...

4. 컨텍스트 매니저

with 문에서 동작합니다. 자원 획득·해제를 안전하게.

클래스 기반:

python
class FileLock:
    def __init__(self, path):
        self.path = path
    def __enter__(self):
        print(f"잠금: {self.path}")
        return self
    def __exit__(self, exc_type, exc, tb):
        print("해제")

with FileLock("data.txt"):
    print("작업 중")

함수 기반 (@contextmanager):

python
from contextlib import contextmanager
import time

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

with timer("작업"):
    sum(range(1_000_000))

5. 실전 — 메모이제이션

python
def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fib(n):
    if n < 2: return n
    return fib(n-1) + fib(n-2)

fib(100)   # 빠름

자주 하는 실수

  1. @wraps 누락 — 디버깅·문서가 망가짐. 항상 붙이기
  2. 데코레이터가 return 안 함 — 결과가 None
  3. @contextmanager 함수에 yield 가 두 개 이상 — 한 번만
  4. __exit__ 가 True 반환 — 예외를 삼킴

FAQ

Q1. 데코레이터는 언제? — 여러 함수에 같은 부가 동작(로깅/인증/캐싱)을 넣을 때.

Q2. with 와 try-finally 의 차이? — with 는 정형화된 try-finally. 가독성·실수 방지.

Q3. 데코레이터를 여러 개 쌓으면? — 아래쪽이 먼저 적용. @A @B def f → f = A(B(f)).

💻 예제 (examples)

실제로 실행해 결과를 확인할 수 있는 예제 코드입니다.

01_basic_decorator.py함수 데코레이터 + wraps
CODE
from functools import wraps

def loud(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f">>> {func.__name__} 호출")
        result = func(*args, **kwargs)
        print(f"<<< 결과: {result}")
        return result
    return wrapper

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

add(3, 5)
print("이름:", add.__name__)
▶ 실행 결과
>>> add 호출
<<< 결과: 8
이름: add
02_param_decorator.py인자 받는 데코레이터 (retry)
CODE
from functools import wraps

def retry(times):
    def deco(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"재시도 {i+1}/{times}: {e}")
            raise
        return wrapper
    return deco

count = 0

@retry(3)
def fragile():
    global count
    count += 1
    if count < 3:
        raise RuntimeError("일시 오류")
    return "성공"

print(fragile())
▶ 실행 결과
재시도 1/3: 일시 오류
재시도 2/3: 일시 오류
성공
03_context_class.py클래스 기반 컨텍스트 매니저
CODE
class FileLock:
    def __init__(self, path):
        self.path = path
    def __enter__(self):
        print(f"잠금: {self.path}")
        return self
    def __exit__(self, exc_type, exc, tb):
        print("해제")

with FileLock("data.txt"):
    print("작업 중")
▶ 실행 결과
잠금: data.txt
작업 중
해제
04_contextmanager.py@contextmanager 함수 기반
CODE
from contextlib import contextmanager
import time

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

with timer("계산"):
    sum(i * i for i in range(1_000_000))
▶ 실행 결과
계산: 0.0521s

📝 과제 (exercises)

직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.

과제 1

@logged 데코레이터

목표: 함수 호출과 결과를 로깅하는 데코레이터를 만든다.

요구사항
  • @logged 적용 시 인자와 결과를 한 줄에 print
  • @wraps 로 메타데이터 보존
💡 힌트

wrapper 안에서 args, kwargs 출력 후 결과 반환

입출력 예시
add(2, 3) -> 5
add(10, 20) -> 30
채점
  • · wraps 사용
  • · 출력 형식 일치
정답 코드 펼치기 / 접기
SOLUTION
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"{func.__name__}{args} -> {result}")
        return result
    return wrapper

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

add(2, 3)
add(10, 20)
▶ 실행 결과
add(2, 3) -> 5
add(10, 20) -> 30
과제 2

@cached 메모이제이션

목표: 결과를 캐시하는 데코레이터를 직접 구현한다.

요구사항
  • @cached 가 같은 인자에 대해 함수 본체를 재실행하지 않는다
  • fib(30) 등 큰 입력에서 즉시 반환
💡 힌트

dict 로 args 키 → 결과 저장

입출력 예시
832040
채점
  • · 인자 동일하면 캐시 사용
  • · 정확한 값 반환
정답 코드 펼치기 / 접기
SOLUTION
from functools import wraps

def cached(func):
    store = {}
    @wraps(func)
    def wrapper(*args):
        if args not in store:
            store[args] = func(*args)
        return store[args]
    return wrapper

@cached
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(30))
▶ 실행 결과
832040
과제 3

with timer 컨텍스트 매니저

목표: @contextmanager 로 실행 시간을 측정한다.

요구사항
  • with timer('label'): ... 형식
  • 블록 종료 시 경과 초를 출력
💡 힌트

time.perf_counter()

yield 전후로 시각 측정

입출력 예시
합계: 0.05s 정도
채점
  • · @contextmanager 사용
  • · 예외 시에도 시간 출력 (선택)
정답 코드 펼치기 / 접기
SOLUTION
from contextlib import contextmanager
import time

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

with timer("합계"):
    s = sum(i for i in range(5_000_000))
print(s)
▶ 실행 결과
합계: 0.21s
12499997500000
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗