@lru_cache 装饰器过多缓存未命中问题

4

如何配置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() 方法来查看实际的缓存命中/未命中情况。 - bnaecker
我知道,但我认为这不是一个明智的默认行为 - 实际上,这些调用在所有实际目的上都是相同的。我想以这样一种方式包装(或替换)lru_cache,以避免对所有不同拼写的相同基础调用的缓存未命中。 - wim
1
@bnaecker:不行,因为问题所尝试实现的行为取决于函数签名,而make_key并不知道。 - user2357112
1
为什么 f()f(x=2) 不被视为相同?在这两种情况下,args=()kwds={'x': 2} 难道不是一样的吗? - mkrieger1
2
@mkrieger1:不对。默认值不是关键字参数。 - user2357112
显示剩余2条评论
1个回答

7
为了做到这一点,您需要通过将参数绑定到形式参数的过程。实际上,这个过程是用C代码实现的,没有公共接口,但是在inspect中有一个(速度较慢)的重新实现。这比通常使用functools.lru_cache慢大约100倍:
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

如果这种方法的性能惩罚太大,您可以使用以下技巧,需要更多的代码重复,但运行速度更快,仅比正常使用lru_cache慢约2倍(有时更快,使用关键字参数):
@functools.lru_cache
def _f(x):
    print("reticulating splines...")
    return x ** 2

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

这种方法使用更快速的C级别参数绑定来规范调用记忆化辅助函数,但需要在三个位置重复函数参数:一次在外部函数签名中,一次在辅助函数签名中,还有一次在对辅助函数的调用中。

运行得很好。你的C语言怎么样?有兴趣直接将“normalize”参数添加到lru_cache中并提交CPython PR吗? - wim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接