如何让 functools.lru_cache 返回新实例?

jmd*_*_dk 8 python caching mutable python-3.x

lru_cache在返回可变对象的函数上使用 Python ,如下所示:

import functools

@functools.lru_cache()
def f():
    x = [0, 1, 2]  # Stand-in for some long computation
    return x
Run Code Online (Sandbox Code Playgroud)

如果我调用这个函数,改变结果并再次调用它,我不会获得一个“新鲜”的、未改变的对象:

a = f()
a.append(3)
b = f()
print(a)  # [0, 1, 2, 3]
print(b)  # [0, 1, 2, 3]
Run Code Online (Sandbox Code Playgroud)

我明白为什么会发生这种情况,但这不是我想要的。解决方法是让调用者负责使用list.copy

a = f().copy()
a.append(3)
b = f().copy()
print(a)  # [0, 1, 2, 3]
print(b)  # [0, 1, 2]
Run Code Online (Sandbox Code Playgroud)

但是我想在里面解决这个问题f。一个漂亮的解决方案是这样的

@functools.lru_cache(copy=True)
def f():
    ...
Run Code Online (Sandbox Code Playgroud)

尽管copy实际上没有任何参数被functools.lru_cache.

关于如何最好地实现这种行为的任何建议?

编辑

根据holdenweb 的回答,这是我的最终实现。functools.lru_cache默认情况下,它的行为与内置copy=True函数完全相同,并在提供时使用复制行为对其进行扩展。

import functools
from copy import deepcopy

def lru_cache(maxsize=128, typed=False, copy=False):
    if not copy:
        return functools.lru_cache(maxsize, typed)
    def decorator(f):
        cached_func = functools.lru_cache(maxsize, typed)(f)
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            return deepcopy(cached_func(*args, **kwargs))
        return wrapper
    return decorator

# Tests below

@lru_cache()
def f():
    x = [0, 1, 2]  # Stand-in for some long computation
    return x

a = f()
a.append(3)
b = f()
print(a)  # [0, 1, 2, 3]
print(b)  # [0, 1, 2, 3]

@lru_cache(copy=True)
def f():
    x = [0, 1, 2]  # Stand-in for some long computation
    return x

a = f()
a.append(3)
b = f()
print(a)  # [0, 1, 2, 3]
print(b)  # [0, 1, 2]
Run Code Online (Sandbox Code Playgroud)

hol*_*web 7

由于lru_cache装饰器的行为不适合您,因此您能做的最好的事情就是构建自己的装饰器,返回它从中获取的内容的副本lru_cache。这意味着使用一组特定参数的第一次调用将创建该对象的两个副本,因为现在缓存将仅保存原型对象。

\n\n

这个问题变得更加困难,因为lru_cache可以接受参数(mazsizetyped),因此调用返回lru_cache一个装饰器。请记住,装饰器将函数作为其参数并(通常)返回一个函数,您必须替换lru_cache为一个接受两个参数并返回一个函数的函数,该函数接受一个函数作为参数并返回一个(包装的)函数,该函数是这不是一个容易让人理解的结构。

\n\n

然后您可以使用以下方式编写您的函数copying_lru_cache装饰器而不是标准函数来编写函数,标准函数现在在更新的装饰器中“手动”应用。

\n\n

根据突变的严重程度,您可能会在不使用深度复制的情况下逃脱,但您没有提供足够的信息来确定这一点。

\n\n

所以你的代码会读取

\n\n
from functools import lru_cache\nfrom copy import deepcopy\n\ndef copying_lru_cache(maxsize=10, typed=False):\n    def decorator(f):\n        cached_func = lru_cache(maxsize=maxsize, typed=typed)(f)\n        def wrapper(*args, **kwargs):\n            return deepcopy(cached_func(*args, **kwargs))\n        return wrapper\n    return decorator\n\n@copying_lru_cache()\ndef f(arg):\n    print(f"Called with {arg}")\n    x = [0, 1, arg]  # Stand-in for some long computation\n    return x\n\nprint(f(1), f(2), f(3), f(1))\n
Run Code Online (Sandbox Code Playgroud)\n\n

这打印

\n\n
Called with 1\nCalled with 2\nCalled with 3\n[0, 1, 1] [0, 1, 2] [0, 1, 3] [0, 1, 1]\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此您需要的缓存行为似乎存在。另请注意文档lru_cache特别警告说

\n\n
\n

一般来说,仅当您想要重用以前计算的值时才应使用 LRU 缓存。因此,缓存具有副作用的函数、需要在每次调用时创建不同的可变对象的函数或不纯的函数(例如 time() 或 random())是没有意义的。

\n
\n