← Назад к вопросам

Как написать декоратор кэширующий значение функции?

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