Functools.lru cache: примеры (PYTHON)

Функция lru_cache для кэширования результатов в Python
Раздел: Функциональное программирование, Кэширование
functools.lru_cache(maxsize: int, typed: bool): callable

Функция functools.lru_cache

Декоратор @lru_cache из модуля functools реализует кэширование результатов выполнения функции по принципу LRU (Least Recently Used - «вытеснение давно неиспользуемых»). Его основное назначение - мемоизация, то есть сохранение результатов вызовов функции для конкретных наборов аргументов с целью избежать повторных вычислений.

Использование декоратора эффективно для функций, которые:

  • Вызываются многократно с одинаковыми аргументами.
  • Имеют «тяжелые» вычисления или ввод-вывод.
  • Являются детерминированными (возвращают одинаковый результат для одних и тех же аргументов).
  • Часто используются в рекурсивных алгоритмах.

Декоратор принимает два необязательных аргумента:

  • maxsize: Максимальное количество записей, хранящихся в кэше. По умолчанию равно 128. Если установлено в None, кэш может расти без ограничений, что может привести к утечке памяти.
  • typed: Булев флаг. Если установлен в True, аргументы разных типов (например, 5 и 5.0) будут кэшироваться отдельно. По умолчанию False.

Декорированная функция получает дополнительные методы: cache_info() (возвращает статистику использования), cache_clear() (очищает кэш) и __wrapped__ (ссылка на исходную функцию).

Примеры использования

Пример базового применения для вычисления факториала:

from functools import lru_cache

@lru_cache(maxsize=32)
def factorial(n):
    if n < 2:
        return 1
    return n * factorial(n-1)

print(factorial(10))
print(factorial.cache_info())
3628800
CacheInfo(hits=8, misses=11, maxsize=32, currsize=11)

Пример с параметром typed:

@lru_cache(typed=True)
def process_value(x):
    return x * 2

print(process_value(5))
print(process_value(5.0))
print(process_value.cache_info())
10
10.0
CacheInfo(hits=0, misses=2, maxsize=128, currsize=2)

Пример без ограничения размера кэша:

@lru_cache(maxsize=None)
def get_heavy_data(key):
    # Имитация тяжелой операции
    return hash(str(key))

print(get_heavy_data('test'))
print(get_heavy_data.cache_info())
-662733300
CacheInfo(hits=0, misses=1, maxsize=None, currsize=1)

Похожие средства в Python

functools.cache: Упрощенная версия lru_cache(maxsize=None), появившаяся в Python 3.9. Не имеет ограничения размера и не вытесняет записи. Подходит для сценариев, где количество уникальных вызовов предсказуемо и невелико.

functools.cached_property: Декоратор для методов класса, который преобразует метод в свойство, вычисляемое единожды за время жизни экземпляра. Результат кэшируется в атрибуте экземпляра. Используется для ленивых вычислений атрибутов объекта.

Сторонние библиотеки: Библиотеки, такие как cachetools, предлагают больше вариантов политик кэширования (TTL, LFU). Их выбор оправдан при необходимости более гибкого управления кэшем.

Аналоги в других языках программирования

JavaScript: Аналогичная функциональность часто реализуется вручную с использованием Map или объектов. Пример простой мемоизации:

function memoize(fn) {
    const cache = new Map();
    return (...args) => {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
}
const slowFunc = (x) => x * 2;
const cachedFunc = memoize(slowFunc);
console.log(cachedFunc(5));
console.log(cachedFunc(5));
10
10

Java: Используется аннотация @Cacheable в Spring Framework или библиотека Guava с CacheBuilder. Пример с Guava:

LoadingCache cache = CacheBuilder.newBuilder()
    .maximumSize(100)
    .build(new CacheLoader() {
        public Integer load(Integer key) {
            return key * 2;
        }
    });
System.out.println(cache.get(5));
10

Go: Стандартной библиотеки не содержит готового декоратора. Реализация требует создания структуры с map и мьютексом для потокобезопасности. Пример простого кэша:

type Cache struct {
    sync.Mutex
    m map[interface{}]interface{}
}
func (c *Cache) Get(key interface{}, compute func() interface{}) interface{} {
    c.Lock()
    defer c.Unlock()
    if v, ok := c.m[key]; ok {
        return v
    }
    v := compute()
    c.m[key] = v
    return v
}

PHP: Функция apc_fetch/apc_store для кэширования в памяти или использование библиотек типа Symfony Cache. Пример:

function memoize($callable) {
    static $cache = [];
    return function() use ($callable, &$cache) {
        $key = serialize(func_get_args());
        if (!isset($cache[$key])) {
            $cache[$key] = call_user_func_array($callable, func_get_args());
        }
        return $cache[$key];
    };
}
$cachedFunc = memoize(fn($x) => $x * 2);
echo $cachedFunc(5);
10

Типичные ошибки

Кэширование функций с нехэшируемыми аргументами: lru_cache требует, чтобы аргументы функции были хэшируемыми (например, списки - нет).

from functools import lru_cache

@lru_cache
def process_list(lst):
    return sum(lst)

process_list([1, 2, 3])
TypeError: unhashable type: 'list'

Изменяемые возвращаемые значения: Кэш возвращает ссылку на один и тот же объект. Если функция возвращает изменяемый объект (список, словарь) и вызывающий код меняет его, это повлияет на все последующие возвращаемые значения из кэша.

@lru_cache
def get_list():
    return []

lst1 = get_list()
lst1.append(1)
lst2 = get_list()
print(lst2)
[1]

Неуместное использование для функций с побочными эффектами: Кэширование подавляет повторные вызовы, что нежелательно для функций, выполняющих ввод-вывод или изменяющих глобальное состояние.

Изменения в последних версиях

В Python 3.8 был добавлен параметр typed.

В Python 3.9 была добавлена функция functools.cache как синтаксический сахар для lru_cache(maxsize=None). Также была улучшена производительность самого декоратора lru_cache.

Начиная с Python 3.10, в functools.lru_cache и functools.cache добавлен метод cache_parameters(), возвращающий словарь с параметрами кэша (maxsize и typed).

Расширенные примеры

Использование с пользовательскими классами, реализующими __hash__ и __eq__:

Пример python
from functools import lru_cache

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __hash__(self):
        return hash((self.x, self.y))
    def __eq__(self, other):
        return isinstance(other, Point) and self.x == other.x and self.y == other.y

@lru_cache
def distance(p1, p2):
    return ((p1.x - p2.x)**2 + (p1.y - p2.y)**2) ** 0.5

p_a = Point(0, 0)
p_b = Point(3, 4)
print(distance(p_a, p_b))
print(distance.cache_info())
5.0
CacheInfo(hits=0, misses=1, maxsize=128, currsize=1)

Кэширование методов класса с использованием @lru_cache на методе, но необходимо учитывать, что первый аргумент self также будет закэширован. Чаще для методов используют @cached_property.

Пример python
class DataService:
    def __init__(self):
        self.call_count = 0
    
    @lru_cache(maxsize=1)
    def get_config(self, env):
        self.call_count += 1  # Эта строка выполнится только при промахе кэша
        return {'env': env, 'data': 'heavy_computation'}

service = DataService()
print(service.get_config('prod'))
print(service.get_config('prod'))
print(f"Call count: {service.call_count}")
{'env': 'prod', 'data': 'heavy_computation'}
{'env': 'prod', 'data': 'heavy_computation'}
Call count: 1

Сброс кэша и использование __wrapped__ для доступа к оригинальной функции:

Пример python
@lru_cache
def square(x):
    return x * x

print(square(5))
square.cache_clear()
print(square.cache_info())
print(square.__wrapped__(5))  # Вызов без кэширования
25
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
25

Использование с рекурсивной функцией, где выгода от кэширования максимальна:

Пример python
@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(50))
print(fibonacci.cache_info())
12586269025
CacheInfo(hits=48, misses=51, maxsize=None, currsize=51)

питон functools.lru_cache function comments

En
Functools.lru cache Memoization decorator