【问题标题】:Reassign a function attribute makes it 'unreachable'重新分配函数属性使其“无法访问”
【发布时间】:2014-10-29 16:45:28
【问题描述】:

我有一个简单的小装饰器,它将函数调用的结果缓存在 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() 而不是为其分配 {} 按预期工作。我只是对分配的原因感兴趣原因解决方案失败。)

【问题讨论】:

    标签: python function-attributes


    【解决方案1】:

    问题出在decorator 模块上。如果您在装饰器中添加一些 print 语句:

    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
    

    如您所见,装饰器内的cache 会根据 clear 函数发生变化。但是,从__main__ 访问的cache 不会改变。在装饰器外部和内部打印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 decorator module 内部,有一行(在它用来制作装饰器的类内部):

    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 模块的问题。

    【讨论】:

    • 从表面上看,我在 SO 上最长的帖子... :)
    【解决方案2】:

    所以@matsjoyce 的回答非常有趣且深入,我知道您已经有了解决方案,但我总是觉得编写自己的装饰器更清晰一些:

    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
    

    【讨论】:

    • 好点。对于这样的问题,它有助于在不深入挖掘模块源代码的情况下查看任何异常情况......
    猜你喜欢
    • 1970-01-01
    • 2020-10-28
    • 1970-01-01
    • 2019-12-03
    • 2015-08-23
    • 1970-01-01
    • 2021-12-08
    • 2014-12-09
    • 1970-01-01
    相关资源
    最近更新 更多