【问题标题】:How can data remain persistent across multiple calls of decorated function?数据如何在多个修饰函数调用中保持持久性?
【发布时间】:2012-08-03 13:32:33
【问题描述】:

以下函数旨在用作存储已计算值的结果的装饰器。如果参数之前已经计算过,该函数将返回存储在cache 字典中的值:

def cached(f):
    f.cache = {}
    def _cachedf(*args):
        if args not in f.cache:
            f.cache[args] = f(*args)

        return f.cache[args]

    return _cachedf

我(错误地)意识到cache 不需要是函数对象的属性。事实上,下面的代码也可以工作:

def cached(f):
    cache = {}   # <---- not an attribute this time!
    def _cachedf(*args):
        if args not in cache:
            cache[args] = f(*args)

        return cache[args]
    return _cachedf

我很难理解cache 对象如何在多个调用中保持不变。多次尝试调用多个缓存函数,都没有发现任何冲突或问题。

谁能帮我理解cache 变量是如何在_cachedf 函数返回后仍然存在的?

【问题讨论】:

    标签: python decorator memoization internals


    【解决方案1】:

    您在此处创建closure:函数_cachedf() 关闭封闭范围内的变量cache。只要函数对象存在,这就会使 cache 保持活跃。

    编辑:也许我应该添加一些关于它在 Python 中如何工作以及 CPython 如何实现它的更多细节。

    让我们看一个更简单的例子:

    def f():
        a = []
        def g():
            a.append(1)
            return len(a)
        return g
    

    交互式解释器中的示例用法

    >>> h = f()
    >>> h()
    1
    >>> h()
    2
    >>> h()
    3
    

    在编译包含函数f() 的模块期间, 编译器看到函数 g() 从 封闭范围并在代码中记住这个外部引用 对应于函数f() 的对象(具体来说,它添加了 将a 命名为f.__code__.co_cellvars)。

    那么当函数f() 被调用时会发生什么?第一行 创建一个新的列表对象并将其绑定到名称a。下一行 创建一个新的函数对象(使用在 模块的编译)并将其绑定到名称g。身体 的g() 此时没有执行,最后是函数对象 被退回。

    由于f()的代码对象有一个注释,名称a是 由本地函数引用,此名称的“单元格”在以下情况下创建 f() 已输入。此单元格包含对实际列表的引用 对象a 绑定到,函数g() 获得对 这个细胞。这样,列表对象和单元格即使保持活动状态 当函数f() 退出时。

    【讨论】:

    • 非常感谢您的解释,您的编辑使事情变得非常清楚。我想知道,为了学习 (C)Python 的内部结构,是否可以通过检查或类似的方式访问您在上一段中提到的“单元”?
    • @rahmu:我纠正了解释中的一个错误(变化不大)。不幸的是,单元格对于 Python 代码是完全透明的,并且总是被它们引用的对象替换,因此无法检查它们。
    【解决方案2】:

    谁能帮我理解缓存变量是如何在 _cachedf 函数返回后仍然存在的?

    这与 Python 的引用计数垃圾收集器有关。 cache 变量将被保存并可访问,因为函数 _cachedf 具有对它的引用,而 cached 的调用者具有对它的引用。当您再次调用该函数时,您仍在使用最初创建的同一函数对象,因此您仍然可以访问缓存。

    在所有对它的引用都被销毁之前,您不会丢失缓存。您可以使用del 运算符来执行此操作。

    例如:

    >>> import time
    >>> def cached(f):
    ...     cache = {}   # <---- not an attribute this time!
    ...     def _cachedf(*args):
    ...         if args not in cache:
    ...             cache[args] = f(*args)
    ...         return cache[args]
    ...     return _cachedf
    ...     
    ... 
    >>> def foo(duration):
    ...     time.sleep(duration)
    ...     return True
    ...     
    ... 
    >>> bob = cached(foo)
    >>> bob(2) # Takes two seconds
    True
    >>> bob(2) # returns instantly
    True
    >>> del bob # Deletes reference to bob (aka _cachedf) which holds ref to cache
    >>> bob = cached(foo)
    >>> bob(2) # takes two seconds
    True
    >>> 
    

    为了记录,你想要实现的是 Memoization,decorator pattern page 提供了一个更完整的记忆装饰器,它做同样的事情,但使用装饰器 类强>。您的代码和基于类的装饰器本质上是相同的,基于类的装饰器在存储之前会检查哈希能力。


    编辑 (2017-02-02):@SiminJie 认为cached(foo)(2) 总是会产生延迟。

    这是因为cached(foo) 返回一个带有新缓存的新函数。当调用cached(foo)(2) 时,会创建一个新的(空)缓存,然后立即调用缓存的函数。

    由于缓存为空且找不到值,它重新运行底层函数。相反,请执行cached_foo = cached(foo),然后多次调用cached_foo(2)。这只会导致第一次通话的延迟。此外,如果用作装饰器,它将按预期工作:

    @cached
    def my_long_function(arg1, arg2):
      return long_operation(arg1,arg2)
    
    my_long_function(1,2) # incurs delay
    my_long_function(1,2) # doesn't
    

    如果您不熟悉装饰器,请查看this answer 以了解上述代码的含义。

    【讨论】:

    • 这对 python 装饰器有什么作用?每次我调用cached(foo)(2) 时,它都不会缓存结果并休眠两秒钟。是否每次调用装饰函数都引用同一个装饰器?
    猜你喜欢
    • 2020-12-19
    • 1970-01-01
    • 1970-01-01
    • 2011-05-30
    • 2015-12-23
    • 1970-01-01
    相关资源
    最近更新 更多