如何获取functools.lru_cache以返回新实例?

时间:2019-02-27 15:50:20

标签: python python-3.x caching mutable

我在返回可变对象的函数上使用Python的lru_cache,例如:

import functools

@functools.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]

我知道为什么会这样,但这不是我想要的。一种解决方法是让呼叫者负责使用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]

2 个答案:

答案 0 :(得分:1)

由于lru_cache装饰器对您而言不合适,因此,您最好的办法就是构建自己的装饰器,该装饰器返回从lru_cache获得的内容的副本。这意味着第一次调用带有一组特定的参数将创建该对象的两个副本,因为现在缓存将仅保存原型对象。

这个问题变得更加困难,因为lru_cache可以接受参数(mazsizetyped),因此对lru_cache call 返回一个装饰器。记住装饰器将一个函数作为其参数,并且(通常)返回一个函数,您必须将lru_cache替换为一个带有两个参数的函数,并返回一个将一个函数作为参数并返回(包裹)功能,这不是一个容易缠住你的头的结构。

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

根据突变的严重程度,您可能无需使用Deepcopy就能逃脱,但是您没有提供足够的信息来确定突变。

这样您的代码便会读取

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的文档特别警告了

  

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

答案 1 :(得分:0)

我想要:

import functools

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

def f():
    return inner_f().copy()

因此,lru_cache进行计算,包装函数进行创建。