如何获取 functools.lru_cache 到 return 个新实例?
How to get functools.lru_cache to return new instances?
我在 returns 可变对象的函数上使用 Python 的 lru_cache
,像这样:
import functools
@functools.lru_cache()
def f():
x = [0, 1, 2] # Stand-in for some long computation
return x
如果我调用此函数,改变结果并再次调用它,我不会获得 "fresh",未改变的对象:
a = f()
a.append(3)
b = f()
print(a) # [0, 1, 2, 3]
print(b) # [0, 1, 2, 3]
我明白为什么会这样,但这不是我想要的。解决方法是让调用者负责使用 list.copy
:
a = f().copy()
a.append(3)
b = f().copy()
print(a) # [0, 1, 2, 3]
print(b) # [0, 1, 2]
不过我想在 f
中解决这个问题。一个漂亮的解决方案是
@functools.lru_cache(copy=True)
def f():
...
虽然没有 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]
因为 lru_cache
装饰器有不适合你的行为,你能做的最好的事情就是构建你自己的装饰器,returns copy 什么它来自 lru_cache
。这将意味着使用一组特定参数的第一次调用将创建该对象的两个副本,因为现在缓存将仅保存原型对象。
这个问题变得更加困难,因为 lru_cache
可以接受参数(mazsize
和 typed
),所以 调用 到 lru_cache
returns 一个 装饰器 。请记住,装饰器将一个函数作为其参数并且(通常)returns 一个函数,您必须将 lru_cache
替换为一个带有两个参数的函数和 returns 一个带有一个参数的函数作为参数的函数和 returns 一个(包装的)函数,这不是一个容易绕过的结构。
然后您将使用 copying_lru_cache
装饰器而不是标准装饰器编写您的函数,标准装饰器现在应用 "manually" 更新后的装饰器。
根据突变的严重程度,您可能会在不使用深度复制的情况下逃脱,但您没有提供足够的信息来确定这一点。
所以你的代码会变成
from functools import lru_cache
from copy import deepcopy
def copying_lru_cache(maxsize=10, typed=False):
def decorator(f):
cached_func = lru_cache(maxsize=maxsize, typed=typed)(f)
def wrapper(*args, **kwargs):
return deepcopy(cached_func(*args, **kwargs))
return wrapper
return decorator
@copying_lru_cache()
def f(arg):
print(f"Called with {arg}")
x = [0, 1, arg] # Stand-in for some long computation
return x
print(f(1), f(2), f(3), f(1))
这会打印
Called with 1
Called with 2
Called with 3
[0, 1, 1] [0, 1, 2] [0, 1, 3] [0, 1, 1]
因此您需要的缓存行为似乎存在。另请注意 lru_cache
的文档特别警告
In general, the LRU cache should only be used when you want to reuse previously computed values. Accordingly, it doesn’t make sense to cache functions with side-effects, functions that need to create distinct mutable objects on each call, or impure functions such as time() or random().
我在 returns 可变对象的函数上使用 Python 的 lru_cache
,像这样:
import functools
@functools.lru_cache()
def f():
x = [0, 1, 2] # Stand-in for some long computation
return x
如果我调用此函数,改变结果并再次调用它,我不会获得 "fresh",未改变的对象:
a = f()
a.append(3)
b = f()
print(a) # [0, 1, 2, 3]
print(b) # [0, 1, 2, 3]
我明白为什么会这样,但这不是我想要的。解决方法是让调用者负责使用 list.copy
:
a = f().copy()
a.append(3)
b = f().copy()
print(a) # [0, 1, 2, 3]
print(b) # [0, 1, 2]
不过我想在 f
中解决这个问题。一个漂亮的解决方案是
@functools.lru_cache(copy=True)
def f():
...
虽然没有 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]
因为 lru_cache
装饰器有不适合你的行为,你能做的最好的事情就是构建你自己的装饰器,returns copy 什么它来自 lru_cache
。这将意味着使用一组特定参数的第一次调用将创建该对象的两个副本,因为现在缓存将仅保存原型对象。
这个问题变得更加困难,因为 lru_cache
可以接受参数(mazsize
和 typed
),所以 调用 到 lru_cache
returns 一个 装饰器 。请记住,装饰器将一个函数作为其参数并且(通常)returns 一个函数,您必须将 lru_cache
替换为一个带有两个参数的函数和 returns 一个带有一个参数的函数作为参数的函数和 returns 一个(包装的)函数,这不是一个容易绕过的结构。
然后您将使用 copying_lru_cache
装饰器而不是标准装饰器编写您的函数,标准装饰器现在应用 "manually" 更新后的装饰器。
根据突变的严重程度,您可能会在不使用深度复制的情况下逃脱,但您没有提供足够的信息来确定这一点。
所以你的代码会变成
from functools import lru_cache
from copy import deepcopy
def copying_lru_cache(maxsize=10, typed=False):
def decorator(f):
cached_func = lru_cache(maxsize=maxsize, typed=typed)(f)
def wrapper(*args, **kwargs):
return deepcopy(cached_func(*args, **kwargs))
return wrapper
return decorator
@copying_lru_cache()
def f(arg):
print(f"Called with {arg}")
x = [0, 1, arg] # Stand-in for some long computation
return x
print(f(1), f(2), f(3), f(1))
这会打印
Called with 1
Called with 2
Called with 3
[0, 1, 1] [0, 1, 2] [0, 1, 3] [0, 1, 1]
因此您需要的缓存行为似乎存在。另请注意 lru_cache
的文档特别警告
In general, the LRU cache should only be used when you want to reuse previously computed values. Accordingly, it doesn’t make sense to cache functions with side-effects, functions that need to create distinct mutable objects on each call, or impure functions such as time() or random().