컴프리헨션으로 표현을 압축했다면, 이번에는 함수를 다루는 함수에 집중합니다. 새 용어가 많아 부담될 수 있으니, "함수를 변수처럼 넘길 수 있다"는 단순한 감각부터 잡고 출발합시다. 예를 들어 "API 호출하기 전에 공통 로그 남기기" 같은 일을 함수 하나로 감싸는 순간 이미 고차 함수를 사용한 것입니다. 이 개념을 토대로 Python 특유의 데코레이터(decorator) 문법이 만들어졌습니다.
이번 글에서 새로 나오는 용어
- 고차 함수: 함수를 값처럼 받아서 쓰거나 새 함수를 돌려주는 함수
- 클로저: 바깥 스코프 값을 기억한 채 실행되는 내부 함수 구조
- 데코레이터:
@문법으로 함수에 공통 전·후처리를 덧씌우는 패턴 - functools.wraps: 데코레이터를 만들 때 원본 함수 이름과 설명을 보존해 주는 도구
개념 정리
학습 메모
- 소요 시간: 60분
- 준비물: 함수 정의·호출, 클로저 감각, pytest로 간단한 테스트 작성
- 학습 목표: 직접 데코레이터를 구현하고
functools.wraps까지 적용해 재시도·타이머 패턴 만들기
- 고차 함수는 함수를 값처럼 다루는 구조입니다.
- 클로저(closure)는 바깥 변수를 기억한 채 실행되는 내부 함수입니다.
- 데코레이터는 함수에 공통 전·후처리를 덧씌우는 문법입니다.
functools.wraps는 포장 함수가 원본 함수의 이름과 설명을 유지하게 도와줍니다.- "Core"로 표시된 섹션만 익혀도 실무에 바로 쓰고, "Optional"은 여유가 있을 때 천천히 살펴보세요.
코드로 이해하기
고차 함수 기초 (Core)
def apply_twice(func, value):
return func(func(value))
def increment(x):
return x + 1
result = apply_twice(increment, 3) # 5
func인자에는 호출 가능한(callable) 객체를 전달합니다.- 함수도 다른 값처럼 변수에 담거나 전달할 수 있다는 점이 파이썬의 유연함입니다.
- 이렇게 간단한 예시를 충분히 납득한 뒤에만 다음 섹션으로 넘어가세요.
내부 함수와 클로저 (Core)
def make_multiplier(factor):
def multiply(value):
return value * factor
return multiply
double = make_multiplier(2)
double(5) # 10
multiply는 바깥 스코프의 factor를 기억합니다. 이렇게 바깥 변수 상태를 잡아둔 함수를 클로저(closure)라고 합니다. 데코레이터는 주로 내부 함수와 클로저 구조로 구현됩니다.
데코레이터 기본형 (Core)
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} 실행 {elapsed:.3f}s")
return result
return wrapper
@timer
def fetch_data():
time.sleep(0.2)
return {"status": "ok"}
@timer구문은fetch_data = timer(fetch_data)와 동일합니다.wrapper내부에서 원본 함수를 호출하며 앞·뒤에 공통 동작을 삽입합니다.*args,**kwargs는 위치·키워드 인자를 그대로 넘겨주는 패턴입니다.
direction: right
caller: "mealbot.fetcher
decorator: "@timer/@retry
wrapper: "wrapper()
target: "원본 함수 호출"
caller -> decorator: "데코레이터를 붙임"
decorator -> wrapper: "wrapper 생성"
wrapper -> target: "*args/**kwargs 전달"
target -> wrapper: "결과 반환"
wrapper -> caller: "결과 다시 전달"
시각적으로 보면 데코레이터는 단순히 함수를 한 겹 더 감싸 공통 코드를 재사용하는 도구라는 점이 명확해집니다. "인자 받아서 실행한다"는 기존 패턴과 크게 다르지 않으니 겁먹지 마세요.
functools.wraps로 메타데이터 유지 (Core)
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
...
return wrapper
functools.wraps 데코레이터는 원본 함수의 이름과 문서 문자열(docstring)을 포장 함수에 복사합니다. 디버깅과 자동 문서화 도구에서 특히 중요합니다.
매개변수를 받는 데코레이터 (Core → Plus)
이제 함수에 붙이는 옵션을 직접 받는 단계입니다. 함수가 겹겹이 싸여 난도가 갑자기 올라가 보일 수 있지만, "함수를 반환한다"는 한 가지 규칙만 지키면 됩니다.
def retry(max_attempts=3):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as exc:
print(f"{attempt}회 실패: {exc}")
raise RuntimeError("재시도 초과")
return wrapper
return decorator
@retry(max_attempts=5)
def unstable_call():
...
데코레이터 자체가 인자를 필요로 할 때는 "데코레이터를 만드는 함수"를 한 번 더 감쌉니다. 함수 레이어가 많아지므로 변수명과 반환 구조를 명확히 유지하세요. 필요하면 손으로 호출 순서를 적어 보며 천천히 따라가도 좋습니다.
실전 예: Slack 알림 재시도 (Core → Plus)
Typer CLI에서 종종 Slack 웹훅을 호출해야 했습니다. API가 가끔 500을 반환해도 바로 포기하고 싶지 않다면 retry 패턴이 딱 맞습니다.
def retry_webhook(max_attempts=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except httpx.HTTPError as exc:
print(f"#{attempt} Slack 전송 실패", exc)
time.sleep(0.5)
raise RuntimeError("웹훅 재시도 초과")
return wrapper
return decorator
@retry_webhook(max_attempts=5)
def send_slack(payload: dict):
httpx.post(WEBHOOK_URL, json=payload, timeout=3)
- 언제 사용하나? 반복되는 API 실패 처리, 공통 캐싱, 공통 인증 등 "모든 함수 호출 전에 같은 준비가 필요한" 시점입니다.
- 무엇을 얻나? 재시도 로직을 한 곳에 두어 CLI·웹앱 어디서든 재사용 가능합니다.
표준 라이브러리의 고차 함수 (Optional)
map(func, iterable): 각 요소에 함수를 적용합니다.filter(func, iterable): 조건을 통과하는 요소만 남깁니다.functools.partial(func, **fixed_args): 일부 인자를 미리 채운 새 함수를 만듭니다.functools.lru_cache: 결과를 메모이즈(캐싱)하는 데코레이터입니다.
고차 함수를 과용하면 오히려 읽기 어려워집니다. 반복 패턴을 줄이고 부수효과를 통제할 때만 선택적으로 사용하세요. 가장 좋은 신호는 "이 코드 복붙을 줄이고 싶다"는 생각이 들 때입니다. 당장 필요하지 않다면 이 구역은 가볍게 훑고 넘어가도 괜찮습니다.
왜 중요할까
- 고차 함수는 함수를 값처럼 다룹니다.
- 데코레이터는 공통 전·후처리를 함수에 주입하는 문법 설탕입니다.
functools모듈을 활용하면 성능과 가독성을 동시에 챙길 수 있습니다.
실습
- 따라 하기:
timer데코레이터를 구현하고@timer를 붙인 함수에서 실행 시간을 출력합니다. - 확장하기:
retry데코레이터를 응용해 HTTP 요청 함수에 적용하고 실패 메시지를 로거에 남기도록 확장합니다. - 디버깅:
functools.wraps를 제거해 함수 이름과 도큐스트링이 어떻게 바뀌는지 확인하고 다시 적용해 IDE 자동완성 문제를 해결합니다. - 완료 기준: 매개변수를 받지 않는 데코레이터와 받는 데코레이터를 각각 만들어 실제 코드에 적용해 봤을 때입니다.
마무리
다음 편에서는 이터레이터·제너레이터를 통해 "필요할 때만 값을 생산"하는 구조를 직접 구현해 보겠습니다.
💬 댓글
이 글에 대한 의견을 남겨주세요