重新分配函数属性使其“不可访问”

2024-05-15 09:55:21 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个简单的小装饰器,它将dict中函数调用的结果缓存为函数属性。你知道吗

from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
    try:
        f.cache[args]
    except KeyError:
        f.cache[args] = f(*args, **kwargs)
    return f.cache[args]

def dynamic_programming(f):
    f.cache = {}
    return decorator(_dynamic_programming, f)

我现在想添加清空缓存的可能性。所以我把dynamic_programming()函数改成这样:

def dynamic_programming(f):
    f.cache = {}
    def clear():
        f.cache = {}
    f.clear = clear
    return decorator(_dynamic_programming, f)

现在假设我用这个小东西来实现一个斐波那契数函数:

@dynamic_programming
def fib(n):
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

>>> fib(4)
5
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}

但现在当我清除缓存时,奇怪的事情发生了:

>>> fib.clear()
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}

或者(在运行新的Python内核的情况下)用另一种方法:

>>> fib.clear()
>>> fib(4)
5
>>> fib.cache
{}

为什么缓存在第一次访问后就不“可访问”,即在调用后调用clear()或在调用后调用clear()时不更改?你知道吗

(顺便说一句,我知道一个正确清除缓存的解决方案:调用f.cache.clear(),而不是将{}分配给它,效果与预期一样。我只对赋值解失败的原因感兴趣。)


Tags: 函数cachereturn属性defargsdynamic装饰
2条回答

问题在于decorator模块。如果将一些print语句添加到decorator中:

from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
    print "Inside decorator", id(f.cache)
    try:
        f.cache[args]
    except KeyError:
        f.cache[args] = f(*args, **kwargs)
    return f.cache[args]

def dynamic_programming(f):
    f.cache = {}
    print "Original cache", id(f.cache)
    def clear():
        f.cache = {}
        print "New cache", id(f.cache)
    f.clear = clear
    return decorator(_dynamic_programming, f)

@dynamic_programming
def fib(n):
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

print fib(4)
print id(fib.cache)
fib.clear()
print id(fib.cache)
print fib(10)
print id(fib.cache)

它输出(跳过重复行):

Original cache 139877501744024
Inside decorator 139877501744024
5
139877501744024
New cache 139877501802208
139877501744024
Inside decorator 139877501802208
89
139877501744024

如您所见,decorator中的cache会根据clear函数的不同而变化。但是,从__main__访问的cache没有改变。在decorator内外打印cache可以获得更清晰的图像(同样,跳过了重复项):

Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
5
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (9,): 55, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
89
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}

正如你所看到的,内部的变化并没有在外部得到回应。问题是在the ^{} module中有一行(在它用来生成decorator的类中):

self.dict = func.__dict__.copy()

然后later

func.__dict__ = getattr(self, 'dict', {})

所以基本上,外面的__dict__和里面的__dict__是不同的。这意味着:

  • __dict__被装饰器复制(没有引用)
  • cache改变时,它改变的是内部__dict__,而不是外部__dict__
  • 因此,由_dynamic_programming使用的cache被清除,但是您无法从外部看到,因为装饰器的__dict__仍然指向旧的cache(正如您在上面看到的,由于内部cache更新,而外部cache保持不变)

总之,decorator模块有问题。你知道吗

所以@matsjoyce的答案非常有趣和深入,我知道你已经有了解决方案,但我总是觉得写我自己的decorators会更清楚一些:

def dynamic_programming(f):
    def wrapper(*args, **kwargs):
        try:
            return wrapper.cache[args]            
        except KeyError:
            res = wrapper.cache[args] = f(*args, **kwargs)
            return res
    wrapper.cache = {}
    wrapper.clear = wrapper.cache.clear
    return wrapper

相关问题 更多 >