본문으로 건너뛰기
김신건의 로그

[Python] functools: cache, partial, reduce, singledispatch

· 수정 · 📖 약 1분 · 544자/단어 #python #functools #stdlib #function
python functools, lru_cache, cache, partial, reduce, singledispatch, cached_property

정의

functools함수형 프로그래밍 도구와 함수 변환 데코레이터를 제공하는 표준 라이브러리. 메모이제이션, 부분 적용, 디스패치, 데코레이터 메타데이터 등을 다룬다.

lru_cache / cache

함수 결과를 메모이제이션. 같은 인자엔 한 번만 계산.

from functools import lru_cache, cache

@cache                      # 3.9+: 무제한 캐시
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

@lru_cache(maxsize=128)     # LRU eviction, 기본 128
def expensive(x, y):
    return slow_compute(x, y)

fib(100)
fib.cache_info()      # CacheInfo(hits=98, misses=101, ...)
fib.cache_clear()
  • 인자는 해시 가능해야 (list 못 받음)
  • 캐시 키는 위치/키워드 인자 둘 다 포함 → f(1, 2)f(a=1, b=2)는 다른 캐시
  • 인스턴스 메서드에 쓰면 self가 키에 포함 → 인스턴스 살아 있는 동안 메모리 점유. 약한 참조 캐시는 별도 구현 필요

cached_property

@property + 캐싱. 인스턴스의 __dict__에 저장.

from functools import cached_property

class DataSet:
    def __init__(self, data):
        self.data = data

    @cached_property
    def stats(self):
        return {"mean": sum(self.data) / len(self.data),
                "max": max(self.data)}

d = DataSet([1, 2, 3])
d.stats       # 계산
d.stats       # 캐시
del d.stats   # 무효화

불변 데이터 가정. 데이터가 바뀌면 캐시 직접 비워야 함.

partial / partialmethod

함수의 일부 인자를 미리 채워 새 함수 생성.

from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube = partial(power, exp=3)

print(square(5))    # 25
print(cube(5))      # 125

# 콜백에 인자 미리 주입
button.on_click(partial(handle_click, user_id=42))

람다보다 빠르고 introspection 친화적 (partial.func, .args, .keywords 노출).

partialmethod는 클래스 메서드에 사용:

from functools import partialmethod

class Service:
    def _request(self, method, url): ...
    get = partialmethod(_request, "GET")
    post = partialmethod(_request, "POST")

reduce

iterable을 누적해 단일 값으로 축약. Python 3에서 functools로 이동.

from functools import reduce

reduce(lambda a, b: a + b, [1, 2, 3, 4])     # 10
reduce(lambda a, b: a + b, [1, 2, 3], 100)   # 106 (초기값)

# 보통 sum, min, max, math.prod로 충분
sum([1, 2, 3, 4])                # 10
import math
math.prod([1, 2, 3, 4])           # 24

reduce는 가독성이 낮아 권장도가 낮음. 명시적 for나 전용 함수 우선.

singledispatch

함수의 첫 번째 인자 타입에 따라 다른 구현으로 디스패치. 단일 디스패치 다형성.

python
from functools import singledispatch

@singledispatch
def render(obj):
  return repr(obj)

@render.register
def _(obj: str):
  return f"text: {obj}"

@render.register
def _(obj: list):
  return f"list of {len(obj)}"

@render.register(dict)
def _(obj):
  return f"dict with keys {list(obj)}"

print(render(42))
print(render("hi"))
print(render([1, 2, 3]))
print(render({"a": 1}))
결과
42
text: hi
list of 3
dict with keys ['a']

타입 별로 함수를 추가할 수 있어 isinstance-체인을 깔끔하게 대체. 메서드 버전은 singledispatchmethod.

from functools import singledispatchmethod

class Renderer:
    @singledispatchmethod
    def render(self, obj):
        return repr(obj)

    @render.register
    def _(self, obj: str):
        return f"text: {obj}"

wraps

데코레이터에서 원본 함수 메타데이터 보존. py-decorator 참고.

from functools import wraps

def log(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        ...
        return fn(*args, **kwargs)
    return wrapper

wraps__name__, __doc__, __qualname__, __module__, __wrapped__ 복사.

update_wrapper

@wraps의 동작을 수동으로. 클래스 기반 데코레이터에서 유용.

from functools import update_wrapper

class Decorator:
    def __init__(self, fn):
        self.fn = fn
        update_wrapper(self, fn)

    def __call__(self, *args, **kwargs):
        return self.fn(*args, **kwargs)

total_ordering

__eq__와 비교 메서드 중 하나만 정의하면 나머지 자동 생성.

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, *parts):
        self.parts = parts

    def __eq__(self, other):
        return self.parts == other.parts

    def __lt__(self, other):
        return self.parts < other.parts

# __le__, __gt__, __ge__ 자동 생성

성능은 직접 구현보다 약간 느림. @dataclass(order=True)가 더 편함.

reduce + operator

reduceoperator 모듈 결합으로 람다 제거.

from functools import reduce
import operator

reduce(operator.add, [1, 2, 3, 4])    # 10
reduce(operator.mul, [1, 2, 3, 4])    # 24

# operator는 itemgetter, attrgetter도 제공
from operator import itemgetter, attrgetter
sorted(items, key=itemgetter("price"))    # lambda d: d["price"] 와 동등
sorted(users, key=attrgetter("name"))     # lambda u: u.name

주의: 가변 인자와 캐시

from functools import lru_cache

@lru_cache
def f(xs):
    return sum(xs)

f([1, 2, 3])    # TypeError: unhashable type: 'list'
f((1, 2, 3))    # OK

list/dict는 해시 안 되므로 tuple/frozenset로 변환해 넘기거나 캐시 안 씀.

캐시 무효화 패턴

@cache
def lookup(key):
    return slow_query(key)

# 캐시 비우기
lookup.cache_clear()

# 특정 키만 무효화는 직접 dict로 구현해야 함 (lru_cache는 단일 키 삭제 미지원)

세밀한 제어가 필요하면 cachetools 라이브러리 또는 직접 dict.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기