декоратор @lru_cache чрезмерно пропускает кэш

#python #function #caching #memoization #functools

#python #функция #кэширование #запоминание #функциональные средства

Вопрос:

Как вы можете настроить lru_cache , чтобы использовать его кэш на основе фактических полученных значений, а не того, как была вызвана функция?

 >>> from functools import lru_cache
>>> @lru_cache
... def f(x=2):
...     print("reticulating splines...")
...     return x ** 2
...
>>> f()
reticulating splines...
4
>>> f(2)
reticulating splines...
4
>>> f(x=2)
reticulating splines...
4
 

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

Комментарии:

1. В документах указано: отдельные шаблоны аргументов могут рассматриваться как отдельные вызовы с отдельными записями в кэше. Например, f(a=1, b=2) и f(b=2, a=1) различаются по порядку аргументов ключевых слов и могут иметь две отдельные записи кэша . Похоже, вы можете использовать этот f.cache_info() метод, чтобы увидеть фактические попадания / промахи в кэш.

2. Я знаю, но я не думаю, что это разумное поведение по умолчанию — вызовы для всех практических целей одинаковы. Я хотел бы обернуть (или заменить) lru_cache таким образом, чтобы избежать промахов кэша для всех различных вариантов написания одного и того же базового вызова.

3. @bnaecker: Нет, потому что поведение, которого пытается достичь вопрос, зависит от сигнатуры функции, которая make_key не знает.

4. Почему f() и f(x=2) не обрабатываются одинаково? Не так args=() ли и kwds={'x': 2} в обоих случаях?

5. @mkrieger1: Нет. Значения по умолчанию не являются аргументами ключевого слова.

Ответ №1:

Для этого вам нужно будет пройти процесс привязки аргументов к формальным параметрам. Фактический процесс выполнения этого реализован в коде C без открытого интерфейса, но в нем есть (гораздо более медленная) повторная inspect реализация. Это примерно в 100 раз медленнее, чем functools.lru_cache при обычном использовании:

 import functools
import inspect

def mycache(f=None, /, **kwargs):
    def inner(f):
        sig = inspect.signature(f)
        f = functools.lru_cache(**kwargs)(f)
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()
            return f(*bound.args, **bound.kwargs)
        return wrapper
    if f:
        return inner(f)
    return inner

@mycache
def f(x):
    print("reticulating splines...")
    return x ** 2
 

Если снижение производительности при таком подходе слишком велико, вы можете вместо этого использовать следующий трюк, который требует большего дублирования кода, но выполняется намного быстрее, всего в 2 раза медленнее, чем lru_cache при обычном использовании (а иногда и быстрее, с аргументами ключевых слов):

 @functools.lru_cache
def _f(x):
    print("reticulating splines...")
    return x ** 2

def f(x=2):
    return _f(x)
 

Это использует гораздо более быструю привязку аргументов уровня C для нормализации вызова записанной вспомогательной функции, но требует дублирования параметров функции 3 раза: один раз в сигнатуре внешней функции, один раз в сигнатуре помощника и один раз при вызове помощника.

Комментарии:

1. Отлично работает. Как твой C? Есть ли интерес собрать CPython PR, добавив аргумент «нормализовать» lru_cache напрямую?