Сообщить об ошибке.

Кэширование возвращаемых значений в декораторах

Декораторы могут обеспечить хороший механизм для кэширования и запоминания. В качестве примера рассмотрим рекурсивное определение последовательности Фибоначчи:

def fib(num):
    if num < 2:
        return num
    return fib(num - 1) + fib(num - 2)

Хотя реализация проста, ее производительность во время выполнения ужасна:

>>> start = time.perf_counter(); fib(20); print('Time run:', time.perf_counter() - start)
# 6765
# Time run: 0.005267535000712087

>>> start = time.perf_counter(); fib(30); print('Time run:', time.perf_counter() - start)
# 832040
# Time run: 0.1682777839996561

Чтобы вычислить десятое число Фибоначчи, вам нужно вычислить только предыдущие числа Фибоначчи, но эта реализация каким-то образом требует колоссальных 177 вычислений. Ситуация быстро ухудшается для fib(20) - 21891 вычисление и почти 2,7 миллиона вычислений для fib(30). Это происходит потому, что код продолжает вычислять числа Фибоначчи, которые уже известны.

Обычным решением является реализация чисел Фибоначчи с использованием цикла for ... in и таблицы подстановки. Простое кэширование вычислений также делает свое дело:

import functools

def cache(func):
    """Кэш предыдущих вызовов функций"""
    cache = {}
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        cache_key = args + tuple(kwargs.items())
        if cache_key not in cache:
            cache[cache_key] = func(*args, **kwargs)
        return cache[cache_key]
    return wrapper

@cache
def fib(num):
    if num < 2:
        return num
    return fib(num - 1) + fib(num - 2)

Кэш работает как таблица подстановки, поэтому теперь функция fib() выполняет необходимые вычисления только один раз. Это сразу заметно по времени выполнения функции. Сравните с предыдущим запуском функции, без кэширующего декоратора в начале материала:

>>> import time
>>> start = time.perf_counter(); fib(20); print('Time run:', time.perf_counter() - start)
# 6765
# Time run: 4.560499928629724e-05

>>> start = time.perf_counter(); fib(30); print('Time run:', time.perf_counter() - start)
# 832040
# Time run: 0.000410601000112365

В стандартной библиотеке доступны 2 кэширующих декоратора:

  • Декоратор @functools.cache модуля functools представляет собой простой легкий неограниченный кеш функций. Иногда называется "memoization".
  • Декоратор @functools.lru_cache кэш LRU. Этот декоратор имеет больше возможностей, чем тот, который представлен для примера.
import functools, time

@functools.lru_cache(maxsize=50)
def fib(num):
    if num < 2:
        return num
    return fib(num - 1) + fib(num - 2)

>>> start = time.perf_counter(); fib(30); print('Time run:', time.perf_counter() - start)
# 832040
# Time run: 0.00011347100007697009

>>> fib.cache_info()
# CacheInfo(hits=28, misses=31, maxsize=50, currsize=31)

Можно использовать метод fib.cache_info(), чтобы увидеть, как работает кэш, так-же можно его настроить, если это необходимо.

В декораторе @functools.lru_cache(maxsize=50), параметр maxsize указывает сколько последних вызовов кэшируется. Значение по умолчанию равно 128, но вы можете указать maxsize=None для кэширования всех вызовов функций. Однако имейте в виду, что это может вызвать проблемы с памятью.