PYTHON · 심층 가이드
Python 함수 완전 정리
*args와 **kwargs로 시그니처를 설계하고 데코레이터·클로저·제너레이터로 상태와 횡단 관심사를 분리하며, lru_cache와 재귀 한계의 실제 경계까지 짚습니다.
Python에서 함수는 호출 가능한 객체일 뿐이라 변수에 담고, 인자로 넘기고, 다른 함수가 만들어 돌려줄 수 있습니다. 데코레이터라는 문법이 특별해 보이지만 f = deco(f)를 @deco로 적은 것에 지나지 않습니다. 이 등식을 손에 익히면 파라미터를 받는 데코레이터가 왜 함수 세 겹이 되는지, 체이닝의 적용 순서가 왜 아래에서 위인지가 한 번에 풀립니다. 반대로 이 등식을 건너뛰면 데코레이터는 계속 외워야 하는 주문으로 남습니다.
기초는 함수 정의 & *args & **kwargs에서 인자 전달 규칙을 잡는 데서 시작합니다. 그 위에 데코레이터 — 함수를 감싸는 함수가 올라가고, 데코레이터 심화 (파라미터)와 데코레이터 체이닝이 같은 아이디어를 접어 올립니다. 상태 축은 클로저 & 제너레이터에서 갈라져 제너레이터 심화와 yield from으로 이어지고, 성능 축은 메모이제이션 (lru_cache)과 부분 함수 (functools.partial)가 담당합니다. 마지막 파이프라인 패턴은 앞의 도구들을 하나로 엮는 연습입니다.
래퍼를 만들 때 functools.wraps를 빼먹으면 원본의 이름·독스트링·시그니처 메타데이터가 사라져 도움말과 디버깅이 무너집니다. 더 조용한 문제는 lru_cache를 인스턴스 메서드에 붙이는 경우입니다. 캐시 키에 self가 들어가면서 인스턴스가 프로세스가 끝날 때까지 참조되어 회수되지 않습니다. 그리고 Python은 꼬리 재귀를 최적화하지 않습니다. 깊은 재귀는 sys.setrecursionlimit으로 억지로 늘리기보다 반복문이나 명시적 스택으로 바꾸는 편이 안전합니다.
01함수 정의 & *args & **kwargs
가변 인자와 키워드 인자로 유연한 함수를 설계합니다.
Python code
# 기본 함수
def add(a: int, b: int) -> int:
return a + b
# 기본값 파라미터
def greet(name: str, prefix: str = "안녕하세요") -> str:
return f"{prefix}, {name}!"
# *args — 가변 위치 인자 (튜플로 받음)
def sum_all(*nums: int) -> int:
return sum(nums)
print(sum_all(1, 2, 3, 4, 5)) # 15
# **kwargs — 가변 키워드 인자 (딕셔너리로 받음)
def create_user(name: str, **attrs) -> dict:
return {"name": name, **attrs}
user = create_user("Alice", age=30, role="admin")
# {'name': 'Alice', 'age': 30, 'role': 'admin'}
# / 와 * 파라미터 구분자 (Python 3.8+)
def strict(pos_only: int, /, normal: str, *, kw_only: bool):
pass
# pos_only는 위치 전용, kw_only는 키워드 전용
strict(1, "hi", kw_only=True)
# Unpacking
nums = [1, 2, 3]
print(sum_all(*nums)) # 15
cfg = {"age": 25, "role": "user"}
print(create_user("Bob", **cfg))함수 시그니처에서 / 앞은 위치 전용, * 뒤는 키워드 전용 파라미터입니다. API 설계 시 명확성을 높입니다.
기본값에 가변 객체(list, dict)를 쓰면 모든 호출이 같은 객체를 공유합니다. def f(items=[]): → def f(items=None): items = items or []로 작성하세요.
02데코레이터 — 함수를 감싸는 함수
데코레이터로 횡단 관심사(로깅, 캐싱, 인증 등)를 분리합니다.
Python code
import functools
import time
# 기본 데코레이터 구조
def timer(func):
@functools.wraps(func) # 원본 메타데이터 보존
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__}: {elapsed:.4f}초")
return result
return wrapper
# 파라미터를 받는 데코레이터 (팩토리)
def retry(times: int = 3, delay: float = 0.5):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"시도 {attempt}/{times} 실패: {e}")
if attempt < times:
time.sleep(delay)
raise RuntimeError(f"{times}번 모두 실패")
return wrapper
return decorator
# 클래스 기반 데코레이터
class Memoize:
def __init__(self, func):
self.func = func
self.cache = {}
functools.update_wrapper(self, func)
def __call__(self, *args):
if args not in self.cache:
self.cache[args] = self.func(*args)
return self.cache[args]
@timer
@retry(times=2, delay=0.1)
def fetch_data(url: str) -> str:
return f"data from {url}"
@Memoize
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
print(fib(35)) # 빠르게 계산@functools.wraps(func)를 반드시 붙이세요. 없으면 wrapper.__name__, wrapper.__doc__이 원본 함수의 것을 잃어버립니다.
데코레이터를 여러 개 쌓으면 아래에서 위로 적용됩니다. @A @B def f()는 A(B(f))입니다.
03클로저 & 제너레이터
클로저로 상태를 캡처하고, yield로 메모리 효율적인 이터레이터를 만듭니다.
Python code
# 클로저 — 외부 함수의 변수를 기억
def make_counter(start: int = 0):
count = start
def counter():
nonlocal count # 외부 변수 수정하려면 nonlocal 필요
count += 1
return count
return counter
c1 = make_counter()
c2 = make_counter(10)
print(c1(), c1(), c1()) # 1 2 3
print(c2(), c2()) # 11 12 (독립적)
# 제너레이터 함수
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 제너레이터는 next() 호출 시에만 실행됨 (lazy)
gen = fibonacci()
fibs = [next(gen) for _ in range(10)]
print(fibs) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# yield from — 하위 제너레이터 위임
def chain(*iterables):
for it in iterables:
yield from it
list(chain([1,2], [3,4], [5])) # [1,2,3,4,5]
# 제너레이터 파이프라인
def read_lines(text):
yield from text.splitlines()
def filter_empty(lines):
yield from (l for l in lines if l.strip())
def to_upper(lines):
yield from (l.upper() for l in lines)
text = "hello
world
python"
result = list(to_upper(filter_empty(read_lines(text))))yield from은 하위 제너레이터를 완전히 위임합니다. 재귀 제너레이터나 파이프라인 구성에 적합합니다.
nonlocal 없이 클로저 내부에서 외부 변수에 대입하면 새 지역 변수가 만들어집니다. UnboundLocalError의 흔한 원인입니다.
04FastAPI 기초
Pydantic + FastAPI로 타입 안전한 REST API 빠르게 구축
Python code
<span class="cm">// FastAPI 기초 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("FastAPI 기초") }PYTHON 공식 문서를 함께 참고하세요.
자주 발생하는 실수에 주의하세요.
05클로저 심화
클로저(closure)는 외부 함수의 변수를 기억하는 내부 함수입니다. 상태를 캡슐화하는 강력한 패턴입니다.
Python code
def make_counter(start=0):
count = start
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter(10)
print(c()) # 11
print(c()) # 12
# 클로저로 설정 캡처
def make_formatter(fmt):
def formatter(value):
return f"{value:{fmt}}"
return formatter
as_percent = make_formatter(".1%")
as_currency = make_formatter(",.0f")
print(as_percent(0.856)) # 85.6%
print(as_currency(1234567)) # 1,234,567
# 클로저 내부 변수 확인
print(as_percent.__closure__[0].cell_contents) # .1%클로저 내부에서 외부 변수를 변경하려면 nonlocal 키워드가 필요합니다.
루프 안에서 클로저를 만들면 모든 클로저가 같은 변수를 참조합니다. 기본 인자로 값을 캡처하세요: lambda x=x: x.
06부분 함수 (functools.partial)
functools.partial로 함수의 일부 인자를 고정하여 새로운 함수를 만듭니다.
Python code
from functools import partial
# 기본 사용
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(3)) # 27
# 로깅 함수 커스터마이징
import logging
debug = partial(print, "[DEBUG]")
error = partial(print, "[ERROR]", end="\n\n")
debug("서버 시작") # [DEBUG] 서버 시작
error("연결 실패") # [ERROR] 연결 실패
# 콜백에서 활용
def fetch_data(url, timeout=30, retry=3):
return f"GET {url} (timeout={timeout}, retry={retry})"
fast_fetch = partial(fetch_data, timeout=5, retry=1)
print(fast_fetch("https://api.example.com"))
# partial 정보 확인
print(fast_fetch.func) # <function fetch_data>
print(fast_fetch.keywords) # {'timeout': 5, 'retry': 1}partial은 콜백 함수에서 추가 인자를 전달할 때 lambda보다 명확하고 pickle 가능합니다.
partial로 고정한 인자를 호출 시 다시 전달하면 TypeError가 발생합니다 (위치 인자의 경우).
07메모이제이션 (lru_cache)
functools.lru_cache로 함수 결과를 캐싱하여 중복 계산을 방지합니다. 재귀 함수 성능을 극적으로 향상시킵니다.
Python code
from functools import lru_cache, cache
import time
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
start = time.perf_counter()
print(fibonacci(100)) # 354224848179261915075
print(f"시간: {time.perf_counter()-start:.6f}초")
# 캐시 정보
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# cache (Python 3.9+) = lru_cache(maxsize=None)
@cache
def factorial(n):
return 1 if n <= 1 else n * factorial(n - 1)
print(factorial(10)) # 3628800
# 캐시 초기화
fibonacci.cache_clear()인자가 해시 가능해야 캐싱됩니다. 리스트 인자는 튜플로 변환하세요.
lru_cache는 인자를 키로 사용하므로 mutable 객체(리스트, 딕셔너리)를 인자로 전달하면 TypeError가 발생합니다.
08데코레이터 심화 (파라미터)
인자를 받는 데코레이터는 3단 중첩 함수로 구현합니다. 실행 횟수 제한, 권한 검사 등에 활용됩니다.
Python code
import functools
from typing import Callable, Any
def retry(max_attempts: int = 3, delay: float = 1.0):
"""재시도 데코레이터 (파라미터 포함)"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exc = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exc = e
print(f"시도 {attempt}/{max_attempts} 실패: {e}")
raise last_exc
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unstable_api():
import random
if random.random() < 0.7:
raise ConnectionError("서버 응답 없음")
return "성공!"
try:
result = unstable_api()
print(result)
except ConnectionError as e:
print(f"최종 실패: {e}")@functools.wraps(func)를 반드시 사용하세요. 원본 함수의 이름, 독스트링 등 메타데이터를 보존합니다.
파라미터 있는 데코레이터에서 괄호를 빼먹으면 (@retry vs @retry()) 함수 자체가 첫 번째 인자로 전달됩니다.
09데코레이터 체이닝
여러 데코레이터를 조합하여 함수에 다중 관심사를 분리 적용합니다. 실행 순서를 이해하는 것이 핵심입니다.
Python code
import functools, time
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__} 호출")
return func(*args, **kwargs)
return wrapper
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[TIME] {func.__name__}: {elapsed:.4f}초")
return result
return wrapper
def validate_positive(func):
@functools.wraps(func)
def wrapper(n, *args, **kwargs):
if n < 0:
raise ValueError(f"양수가 필요합니다: {n}")
return func(n, *args, **kwargs)
return wrapper
# 아래→위 순서로 감싸짐, 위→아래 순서로 실행
@log_call # 3. 가장 바깥
@timer # 2. 중간
@validate_positive # 1. 가장 안쪽
def factorial(n):
return 1 if n <= 1 else n * factorial(n - 1)
print(factorial(10))데코레이터는 아래에서 위로 적용되고, 호출 시에는 위에서 아래로 실행됩니다.
데코레이터 순서를 바꾸면 동작이 달라집니다. 예를 들어 @timer를 @log_call 위에 두면 로깅 시간도 포함됩니다.
10재귀 한계
Python의 재귀 깊이 제한과 이를 다루는 방법입니다. 기본 한계는 1000이며 스택 오버플로를 방지합니다.
Python code
import sys
# 현재 재귀 한계 확인
print(f"기본 재귀 한계: {sys.getrecursionlimit()}")
# 재귀 한계 변경 (주의해서 사용)
sys.setrecursionlimit(5000)
# 재귀를 반복으로 변환 (꼬리 재귀 최적화)
def factorial_recursive(n):
if n <= 1:
return 1
return n * factorial_recursive(n - 1)
def factorial_iterative(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
# 트램폴린 기법으로 재귀 제한 우회
def trampoline(func):
def wrapper(*args):
result = func(*args)
while callable(result):
result = result()
return result
return wrapper
def _factorial(n, acc=1):
if n <= 1:
return acc
return lambda: _factorial(n - 1, n * acc)
factorial_safe = trampoline(_factorial)
print(factorial_safe(10000)) # 큰 수도 가능깊은 재귀가 필요하면 반복문으로 변환하거나 트램폴린 기법을 사용하세요.
sys.setrecursionlimit()을 너무 높이면 실제 스택 오버플로(segfault)가 발생할 수 있습니다.
11꼬리 재귀 최적화
Python은 꼬리 재귀 최적화를 지원하지 않지만, 데코레이터로 비슷한 효과를 구현할 수 있습니다.
Python code
# 꼬리 재귀 → 반복 변환 데코레이터
class TailCall:
def __init__(self, func, args, kwargs):
self.func = func
self.args = args
self.kwargs = kwargs
def tail_recursive(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
while isinstance(result, TailCall):
result = result.func(*result.args, **result.kwargs)
return result
def tail_call(*args, **kwargs):
return TailCall(wrapper, args, kwargs)
wrapper.tail_call = tail_call
return wrapper
@tail_recursive
def sum_to(n, acc=0):
if n == 0:
return acc
return sum_to.tail_call(n - 1, acc + n)
print(sum_to(100000)) # 5000050000 (스택 오버플로 없음)
# 일반 재귀 대비
def sum_to_naive(n, acc=0):
if n == 0:
return acc
return sum_to_naive(n - 1, acc + n)
# sum_to_naive(100000) # RecursionError!꼬리 재귀 패턴: 재귀 호출이 함수의 마지막 연산이고, 누적값을 인자로 전달합니다.
Python은 공식적으로 TCO를 지원하지 않습니다. 성능이 중요하면 처음부터 반복문으로 작성하세요.
12제너레이터 심화
제너레이터의 send(), throw(), close() 메서드로 양방향 통신을 구현합니다.
Python code
# send()로 값 주입
def accumulator():
total = 0
while True:
value = yield total
if value is None:
break
total += value
gen = accumulator()
next(gen) # 제너레이터 시작 (첫 yield까지)
print(gen.send(10)) # 10
print(gen.send(20)) # 30
print(gen.send(5)) # 35
# 제너레이터로 이동 평균 계산
def moving_average():
total = 0.0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
avg = moving_average()
next(avg) # 초기화
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
# close()로 제너레이터 종료
avg.close()send()를 사용하기 전에 반드시 next()로 제너레이터를 첫 yield까지 진행시켜야 합니다.
제너레이터에 처음 send()를 호출할 때 None이 아닌 값을 보내면 TypeError가 발생합니다.
13코루틴 제너레이터
제너레이터 기반 코루틴으로 협력적 멀티태스킹을 구현합니다. async/await의 기반이 되는 개념입니다.
Python code
# 간단한 태스크 스케줄러
from collections import deque
def task_a():
for i in range(3):
print(f" Task A: 단계 {i}")
yield
def task_b():
for i in range(5):
print(f" Task B: 단계 {i}")
yield
def scheduler(tasks):
queue = deque(tasks)
while queue:
task = queue.popleft()
try:
next(task)
queue.append(task)
except StopIteration:
print(f" 태스크 완료")
print("=== 코루틴 스케줄러 ===")
scheduler([task_a(), task_b()])
# 파이프라인 코루틴
def coroutine(func):
def wrapper(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return wrapper
@coroutine
def printer():
while True:
line = yield
print(f"출력: {line}")
p = printer()
p.send("Hello")
p.send("World")현대 Python에서는 제너레이터 코루틴 대신 async/await를 사용하세요. 이 패턴은 원리 이해를 위해 유용합니다.
제너레이터 코루틴과 async 코루틴을 혼합하면 예상치 못한 동작이 발생합니다. 하나의 패러다임을 일관되게 사용하세요.
14yield from
yield from으로 하위 제너레이터에 위임하여 제너레이터 체이닝을 간결하게 합니다.
Python code
# yield from 기본
def inner():
yield 1
yield 2
yield 3
def outer():
yield 0
yield from inner() # 하위 제너레이터에 위임
yield 4
print(list(outer())) # [0, 1, 2, 3, 4]
# 트리 순회에 활용
def flatten(items):
for item in items:
if isinstance(item, (list, tuple)):
yield from flatten(item)
else:
yield item
nested = [1, [2, [3, 4], 5], [6, 7]]
print(list(flatten(nested))) # [1, 2, 3, 4, 5, 6, 7]
# yield from의 반환값
def sub_gen():
yield "a"
yield "b"
return "완료!"
def main_gen():
result = yield from sub_gen()
print(f"sub_gen 반환: {result}")
for v in main_gen():
print(v)
# a, b, sub_gen 반환: 완료!yield from은 하위 제너레이터의 send()와 throw()도 자동으로 전달합니다.
yield from은 이터러블만 받습니다. 일반 함수의 반환값을 전달하려면 yield를 사용하세요.
15함수 어노테이션
함수 어노테이션으로 매개변수와 반환값의 타입을 명시하고, 런타임에서 검사하는 방법입니다.
Python code
from typing import Callable, Any
import inspect
# 기본 어노테이션
def greet(name: str, times: int = 1) -> str:
return (f"안녕, {name}! " * times).strip()
# 어노테이션 접근
print(greet.__annotations__)
# {'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}
# 콜러블 타입
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
print(apply(lambda x, y: x + y, 3, 4)) # 7
# 런타임 타입 검사 데코레이터
def typecheck(func):
hints = func.__annotations__
sig = inspect.signature(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
for name, value in bound.arguments.items():
if name in hints and not isinstance(value, hints[name]):
raise TypeError(
f"{name}은(는) {hints[name].__name__}이어야 합니다"
)
return func(*args, **kwargs)
return wrapper
@typecheck
def add(a: int, b: int) -> int:
return a + b
print(add(1, 2)) # 3inspect.signature()로 함수의 매개변수 정보를 상세하게 조회할 수 있습니다.
Python의 타입 힌트는 기본적으로 런타임에 강제되지 않습니다. 런타임 검증이 필요하면 별도 도구(pydantic, beartype)를 사용하세요.
16콜러블 객체
__call__ 메서드를 구현하여 인스턴스를 함수처럼 호출할 수 있게 만듭니다.
Python code
class Multiplier:
"""상태를 가진 콜러블"""
def __init__(self, factor: int):
self.factor = factor
self.call_count = 0
def __call__(self, value):
self.call_count += 1
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(f"호출 횟수: {double.call_count}") # 1
# 함수처럼 사용
numbers = [1, 2, 3, 4, 5]
print(list(map(double, numbers))) # [2, 4, 6, 8, 10]
# callable() 확인
print(callable(double)) # True
print(callable(42)) # False
# 설정 가능한 밸리데이터
class RangeValidator:
def __init__(self, min_val, max_val):
self.min_val = min_val
self.max_val = max_val
def __call__(self, value):
if not self.min_val <= value <= self.max_val:
raise ValueError(f"{value}은 {self.min_val}-{self.max_val} 범위 밖")
return value
validate_age = RangeValidator(0, 150)
print(validate_age(25)) # 25콜러블 객체는 상태가 있는 함수가 필요할 때 클로저의 대안으로 사용할 수 있습니다.
__call__을 정의하지 않은 객체를 호출하면 TypeError: object is not callable이 발생합니다.
17단일 디스패치
functools.singledispatch로 인자 타입에 따라 다른 구현을 실행하는 함수 오버로딩을 구현합니다.
Python code
from functools import singledispatch
@singledispatch
def process(data):
raise NotImplementedError(f"{type(data)}은 지원하지 않음")
@process.register(str)
def _(data):
return f"문자열 처리: {data.upper()}"
@process.register(int)
@process.register(float)
def _(data):
return f"숫자 처리: {data * 2}"
@process.register(list)
def _(data):
return f"리스트 처리: {len(data)}개 항목"
@process.register(dict)
def _(data):
return f"딕셔너리 처리: 키={list(data.keys())}"
print(process("hello")) # 문자열 처리: HELLO
print(process(42)) # 숫자 처리: 84
print(process([1, 2, 3])) # 리스트 처리: 3개 항목
print(process({"a": 1})) # 딕셔너리 처리: 키=['a']
# 등록된 구현 확인
print(process.registry.keys())클래스 메서드에는 singledispatchmethod를 사용하세요 (Python 3.8+).
singledispatch는 첫 번째 인자의 타입만 검사합니다. 여러 인자 타입 조합은 지원하지 않습니다.
18함수 체이닝
여러 함수를 순차적으로 연결하여 데이터 변환 파이프라인을 구성합니다.
Python code
from typing import Callable, TypeVar
T = TypeVar("T")
class Chain:
"""함수 체이닝을 위한 래퍼"""
def __init__(self, value):
self._value = value
def then(self, func: Callable) -> "Chain":
return Chain(func(self._value))
@property
def value(self):
return self._value
# 체이닝 사용
result = (
Chain(" Hello, World! ")
.then(str.strip)
.then(str.lower)
.then(lambda s: s.replace("world", "python"))
.then(str.title)
.value
)
print(result) # Hello, Python!
# 리스트 데이터 파이프라인
data = Chain([3, 1, 4, 1, 5, 9, 2, 6])
result = (
data
.then(sorted)
.then(lambda lst: [x for x in lst if x > 2])
.then(lambda lst: [x ** 2 for x in lst])
.value
)
print(result) # [9, 16, 25, 36, 81]함수 체이닝은 데이터 변환 파이프라인을 읽기 쉽게 만들어줍니다.
체이닝 중 하나의 함수가 None을 반환하면 다음 체인에서 오류가 발생합니다. 각 함수가 값을 반환하는지 확인하세요.
19파이프라인 패턴
함수들을 파이프라인으로 연결하여 데이터를 순차적으로 처리하는 함수형 패턴입니다.
Python code
from functools import reduce
from typing import Callable, Any
# pipe: 왼쪽에서 오른쪽으로 합성
def pipe(*funcs: Callable) -> Callable:
def piped(data):
return reduce(lambda v, f: f(v), funcs, data)
return piped
# compose: 오른쪽에서 왼쪽으로 합성
def compose(*funcs: Callable) -> Callable:
return pipe(*reversed(funcs))
# 개별 변환 함수
def normalize(text: str) -> str:
return text.strip().lower()
def remove_punctuation(text: str) -> str:
return "".join(c for c in text if c.isalnum() or c.isspace())
def tokenize(text: str) -> list[str]:
return text.split()
# 파이프라인 조합
text_pipeline = pipe(normalize, remove_punctuation, tokenize)
result = text_pipeline(" Hello, World! Python 3.12 ")
print(result) # ['hello', 'world', 'python', '312']
# compose로 같은 결과
process = compose(tokenize, remove_punctuation, normalize)
print(process(" Test, Data! ")) # ['test', 'data']pipe는 데이터 흐름 순서대로, compose는 수학적 합성 순서(g∘f)로 함수를 연결합니다.
파이프라인의 각 함수는 이전 함수의 출력 타입을 입력으로 받아야 합니다. 타입 불일치 시 런타임 오류가 발생합니다.
정리하며
@deco는f = deco(f)의 축약입니다. 이 등식으로 체이닝 순서와 인자 데코레이터를 해석합니다.- 모든 래퍼 함수에
functools.wraps를 붙여 이름과 독스트링을 보존합니다. - 인스턴스 메서드에
lru_cache를 걸면self가 캐시에 붙들려 객체가 회수되지 않습니다. - 꼬리 재귀 최적화는 없습니다. 깊은 재귀는 반복문이나 명시적 스택으로 옮깁니다.
더 깊이 들어가고 싶다면 Python 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.