← Назад к вопросам
Как написать декоратор кэширующий значение функции?
2.0 Middle🔥 201 комментариев
#Python Core#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Кэширующий декоратор для функций
Основная идея
Кэширование (memoization) — это техника оптимизации, которая сохраняет результаты вызовов функции и возвращает кэшированные результаты при повторном вызове с теми же аргументами. Декоратор для кэширования оборачивает функцию и управляет этим процессом.
Простой кэширующий декоратор
import functools
def cache(func):
"""Простой кэширующий декоратор"""
cached = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Создаём ключ из аргументов
key = (args, tuple(sorted(kwargs.items())))
if key in cached:
print(f"Возврат из кэша для {func.__name__}{args}")
return cached[key]
# Вычисляем результат и кэшируем
result = func(*args, **kwargs)
cached[key] = result
return result
return wrapper
@cache
def fibonacci(n):
"""Вычисление чисел Фибоначчи"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Использование
print(fibonacci(10)) # Первый вызов - вычисляет
print(fibonacci(10)) # Второй вызов - из кэша
Проблема с неизменяемостью ключей
Если аргументы содержат изменяемые типы (списки, словари), они не могут быть ключом словаря:
# ❌ Проблема
@cache
def process_list(items):
return sum(items)
process_list([1, 2, 3]) # TypeError: unhashable type: list
Продвинутый кэширующий декоратор с обработкой ошибок
import functools
from typing import Any, Callable, TypeVar
F = TypeVar(F, bound=Callable[..., Any])
def cache_with_limit(max_size: int = 128):
"""Кэширующий декоратор с ограничением размера"""
def decorator(func: F) -> F:
cached = {}
call_count = {}
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
# Создаём хешируемый ключ
try:
key = (args, tuple(sorted(kwargs.items())))
except TypeError:
# Если есть неизменяемые аргументы, пропускаем кэширование
return func(*args, **kwargs)
if key in cached:
call_count[key] += 1
return cached[key]
# Если кэш переполнен, удаляем наименее используемый элемент
if len(cached) >= max_size:
# LRU (Least Recently Used) стратегия
lru_key = min(call_count, key=call_count.get)
del cached[lru_key]
del call_count[lru_key]
result = func(*args, **kwargs)
cached[key] = result
call_count[key] = 1
return result
# Методы для управления кэшем
wrapper.cache_clear = lambda: (cached.clear(), call_count.clear())
wrapper.cache_info = lambda: f"Размер: {len(cached)}, макс: {max_size}"
return wrapper
return decorator
@cache_with_limit(max_size=100)
def expensive_calculation(n: int) -> int:
print(f"Вычисляю для {n}...")
return n ** 2
print(expensive_calculation(5)) # Вычисляет
print(expensive_calculation(5)) # Из кэша
expensive_calculation.cache_info() # "Размер: 1, макс: 100"
Встроенное решение: functools.lru_cache
Python предоставляет встроенный декоратор для кэширования:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
"""Кэширование с LRU стратегией вытеснения"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Использование
print(fibonacci(100)) # Быстро благодаря кэшу
print(fibonacci.cache_info()) # CacheInfo(hits=98, misses=101, ...)
fibonacci.cache_clear() # Очистка кэша
Для функций с аргументами любого типа:
from functools import lru_cache
@lru_cache(maxsize=None) # Неограниченный кэш
def process_data(data: tuple) -> str:
"""Работает с кортежами, но не со списками"""
return f"Обработано: {len(data)} элементов"
# Аргументы должны быть хешируемыми
print(process_data((1, 2, 3))) # OK
# process_data([1, 2, 3]) # TypeError
Кэширующий декоратор с TTL (Time To Live)
import functools
import time
from typing import Any, Callable, TypeVar
F = TypeVar(F, bound=Callable[..., Any])
def cache_with_ttl(ttl_seconds: int):
"""Кэш с временем жизни"""
def decorator(func: F) -> F:
cached = {} # {key: (value, timestamp)}
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
try:
key = (args, tuple(sorted(kwargs.items())))
except TypeError:
return func(*args, **kwargs)
now = time.time()
if key in cached:
value, timestamp = cached[key]
if now - timestamp < ttl_seconds:
return value
else:
del cached[key]
result = func(*args, **kwargs)
cached[key] = (result, now)
return result
wrapper.cache_clear = lambda: cached.clear()
return wrapper
return decorator
@cache_with_ttl(ttl_seconds=60)
def fetch_weather(city: str) -> str:
"""Кэшировать ответ на 60 секунд"""
print(f"Запрашиваю погоду для {city}...")
return f"Погода в {city}: +20°C"
print(fetch_weather("Москва")) # Запрос к API
time.sleep(1)
print(fetch_weather("Москва")) # Из кэша
time.sleep(60)
print(fetch_weather("Москва")) # Кэш истёк, новый запрос
Кэширующий декоратор для методов класса
import functools
class cached_property:
"""Свойство, вычисляемое один раз и кэшируемое"""
def __init__(self, func):
self.func = func
self.__doc__ = func.__doc__
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
class User:
def __init__(self, user_id: int):
self.user_id = user_id
@cached_property
def profile(self):
"""Получить профиль (вычисляется один раз)"""
print(f"Загружаю профиль пользователя {self.user_id}...")
return {"name": "John", "email": "john@example.com"}
user = User(1)
print(user.profile) # Вычисляет
print(user.profile) # Из кэша (из __dict__)
Сравнение подходов
| Подход | Преимущества | Недостатки |
|---|---|---|
| Простой dict-кэш | Полный контроль, простой код | Нет ограничений памяти |
| functools.lru_cache | Встроено, LRU вытеснение, быстро | Только хешируемые аргументы |
| cache_with_ttl | Автоматическое устаревание | Сложнее реализовать |
| @cached_property | Удобно для методов класса | Работает только для свойств |
Практический пример: вычисление факториала
from functools import lru_cache
@lru_cache(maxsize=256)
def factorial(n: int) -> int:
"""Факториал с кэшированием"""
if n < 0:
raise ValueError("n должно быть >= 0")
if n <= 1:
return 1
return n * factorial(n - 1)
# Использование
print(factorial(10)) # Вычисляет
print(factorial(10)) # Из кэша
print(factorial(15)) # Использует кэшированные значения для 0-10
print(factorial.cache_info())
# CacheInfo(hits=6, misses=16, maxsize=256, currsize=16)
Выводы
- Используй встроенный functools.lru_cache для большинства случаев
- Для сложных случаев (TTL, неизменяемые аргументы) пиши свой декоратор
- Помни об ограничениях памяти при кэшировании больших объектов
- Тестируй кэширование — убедись, что оно действительно ускоряет код
- Для методов класса используй @cached_property