🚀
고급 (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 wrapper3. 인자 받는 데코레이터
데코레이터를 만드는 함수 — 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) # 빠름자주 하는 실수
- @wraps 누락 — 디버깅·문서가 망가짐. 항상 붙이기
- 데코레이터가 return 안 함 — 결과가 None
- @contextmanager 함수에 yield 가 두 개 이상 — 한 번만
- __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
이름: add02_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