2

我经常在 Python 中进行交互工作,其中涉及一些我不想经常重复的昂贵操作。我通常会运行我经常处理的任何 Python 文件。

如果我写:

import functools32

@functools32.lru_cache()
def square(x):
    print "Squaring", x
    return x*x

我得到这种行为:

>>> square(10)
Squaring 10
100
>>> square(10)
100
>>> runfile(...)
>>> square(10)
Squaring 10
100

也就是说,重新运行文件会清除缓存。这有效:

try:
    safe_square
except NameError:
    @functools32.lru_cache()
    def safe_square(x):
        print "Squaring", x
        return x*x

但是当函数很长时,将其定义放在try块中会感觉很奇怪。我可以这样做:

def _square(x):
    print "Squaring", x
    return x*x

try:
    safe_square_2
except NameError:
    safe_square_2 = functools32.lru_cache()(_square)

但感觉很做作(例如,在没有'@'符号的情况下调用装饰器)

有没有一种简单的方法来处理这个问题,比如:

@non_resetting_lru_cache()
def square(x):
    print "Squaring", x
    return x*x

?

4

4 回答 4

7

编写要在同一会话中重复执行的脚本是一件很奇怪的事情。

我可以理解您为什么要这样做,但这仍然很奇怪,而且我认为通过看起来有点奇怪来暴露这种奇怪的代码并有解释它的评论是不合理的。

但是,您使事情变得比必要的更丑陋。

首先,您可以这样做:

@functools32.lru_cache()
def _square(x):
    print "Squaring", x
    return x*x

try:
    safe_square_2
except NameError:
    safe_square_2 = _square

将缓存附加到新_square定义没有任何害处。它不会浪费任何时间,也不会浪费超过几个字节的存储空间,而且最重要的是,它不会影响之前 _square定义的缓存。这就是闭包的全部意义所在。


递归函数存在潜在问题。它已经存在于您的工作方式中,缓存不会以任何方式添加到其中,但您可能只是因为缓存而注意到它,所以我将对其进行解释并展示如何修复它。考虑这个函数:

@lru_cache()
def _fact(n):
    if n < 2:
        return 1
    return _fact(n-1) * n

当您重新执行脚本时,即使您引用了 old _fact,它最终也会调用 new _fact,因为它是_fact作为全局名称访问的。@lru_cache它与;无关 删除它,旧函数最终仍会调用新的_fact.

但是如果你使用上面的重命名技巧,你可以调用重命名的版本:

@lru_cache()
def _fact(n):
    if n < 2:
        return 1
    return fact(n-1) * n

现在老人_fact会打电话fact,这仍然是老人_fact。同样,无论有没有缓存装饰器,它的工作原理都是一样的。


除了最初的技巧之外,您还可以将整个模式分解为一个简单的装饰器。我会在下面一步一步解释,或者看这篇博文


无论如何,即使使用不那么丑陋的版本,它仍然有点丑陋和冗长。如果你这样做几十次,我的“好吧,它应该看起来有点难看”的理由会很快消失。所以,你会想用你总是排除丑陋的方式来处理这个问题:将它包装在一个函数中。

您不能真正将名称作为 Python 中的对象传递。而且您不想使用可怕的框架黑客来处理这个问题。因此,您必须将名称作为字符串传递。像这样:

globals().setdefault('fact', _fact)

globals函数只返回当前作用域的全局字典。这是 a dict,这意味着它有方法,这意味着如果它还没有值,setdefault这将把全局名称设置为该fact值,但如果有,则什么也不做。_fact这正是你想要的。(您也可以setattr在当前模块上使用,但我认为这种方式强调该脚本旨在(重复)在其他人的范围内执行,而不是用作模块。)

所以,这里包含在一个函数中:

def new_bind(name, value):
    globals().setdefault(name, value)

......你可以把它变成一个装饰器几乎是微不足道的:

def new_bind(name):
    def wrap(func):
        globals().setdefault(name, func)
        return func
    return wrap

您可以像这样使用它:

@new_bind('foo')
def _foo():
    print(1)

但是等等,还有更多!func得到的new_bind将有一个__name__,对吗?如果您坚持命名约定,例如“私有”名称必须是带有_前缀的“公共”名称,我们可以这样做:

def new_bind(func):
    assert func.__name__[0] == '_'
    globals().setdefault(func.__name__[1:], func)
    return func

你可以看到这是怎么回事:

@new_bind
@lru_cache()
def _square(x):
    print "Squaring", x
    return x*x

有一个小问题:如果您使用任何其他没有正确包装函数的装饰器,它们会破坏您的命名约定。所以……不要那样做。:)


而且我认为这完全符合您在每种边缘情况下想要的方式。特别是,如果您已编辑源并希望使用新缓存强制执行新定义,del square则在重新运行文件之前,它可以工作。


当然,如果您想将这两个装饰器合并为一个,这样做很简单,并将其命名为non_resetting_lru_cache.

但是,我会将它们分开。我认为他们的所作所为更明显。如果你想包裹另一个装饰器@lru_cache,你可能仍然想@new_bind成为最外层的装饰器,对吧?


如果你想放入new_bind一个可以导入的模块怎么办?然后它就不起作用了,因为它将引用该模块的全局变量,而不是您当前正在编写的那个。

您可以通过显式传递您的globalsdict、模块对象或模块名称作为参数来解决此问题,例如@new_bind(__name__),因此它可以找到您的全局变量而不是它的全局变量。但这是丑陋和重复的。

您也可以使用丑陋的框架黑客来修复它。至少在 CPython 中,sys._getframe()可用于获取调用者的框架,并frame objects引用其全局命名空间,因此:

def new_bind(func):
    assert func.__name__[0] == '_'
    g = sys._getframe(1).f_globals
    g.setdefault(func.__name__[1:], func)
    return func

请注意文档中的大框告诉您这是一个“实现细节”,可能仅适用于 CPython,并且“仅用于内部和专门目的”。认真对待这件事。每当有人对可以在纯 Python 中实现的 stdlib 或内置函数有一个很酷的想法时,但只能通过 using _getframe,它通常被视为根本无法在纯 Python 中实现的想法。但是,如果您知道自己在做什么,并且想使用它,并且只关心当前版本的 CPython,那么它会起作用。

于 2013-11-20T03:11:44.220 回答
5

标准库中没有persistent_lru_cache。但是你可以很容易地构建一个。

functools源代码直接从docs链接,因为是与示例代码一样有用的模块之一,因为它可以直接使用它。

如您所见,缓存只是一个dict. 如果将其替换为 a shelf,它将自动变为持久性:

def persistent_lru_cache(filename, maxsize=128, typed=False):
    """new docstring explaining what dbpath does"""
    # same code as before up to here
    def decorating_function(user_function):
        cache = shelve.open(filename)
        # same code as before from here on.

当然,只有当你的参数是字符串时才有效。而且可能有点慢。

因此,您可能希望将其保留为 in-memory dict,只需编写将其腌制到文件的代码atexit,并在启动时从文件中恢复它:

    def decorating_function(user_function):
        # ...

        try:
            with open(filename, 'rb') as f:
                cache = pickle.load(f)
            except:
                cache = {}
        def cache_save():
            with lock:
                with open(filename, 'wb') as f:
                    pickle.dump(cache, f)
        atexit.register(cache_save)

        # …
        wrapper.cache_save = cache_save
        wrapper.cache_filename = filename

或者,如果您希望它写入每 N 个新值(这样您就不会丢失整个缓存,例如,一个_exit或一个段错误或有人拉线),请将其添加到 的第二个和第三个版本wrapper,就在misses += 1

            if misses % N == 0:
                cache_save()

请参阅此处以获取到目前为止所有内容的工作版本(save_every用作“N”参数,并默认为1,您可能在现实生活中不想要)。

如果您想变得非常聪明,也许可以复制缓存并将其保存在后台线程中。

您可能希望扩展cache_info以包含诸如缓存写入次数、自上次缓存写入以来未命中次数、启动时缓存中的条目数等内容……</p>

可能还有其他方法可以改善这一点。

从快速测试来看,使用save_every=1,这使得缓存get_pepfib(来自functools文档)持久,没有可测量的减速get_pep和非常小的减速到fib第一次(请注意,fib(100)有 100097 次命中与 101 次未命中......),并且当你重新运行它时,当然会有很大的加速get_pep(但不是)。fib所以,正是你所期望的。

于 2013-11-20T01:52:13.357 回答
0

我不能说我不会只使用@abarnert 的“丑陋框架黑客”,但这里是要求您传入调用模块的全局字典的版本。我认为值得发布,因为带参数的装饰器函数与不带参数的装饰器函数很棘手且有意义的不同

def create_if_not_exists_2(my_globals):
    def wrap(func):
        if "_" != func.__name__[0]:
            raise Exception("Function names used in cine must begin with'_'")
        my_globals.setdefault(func.__name__[1:], func)
        def wrapped(*args):
            func(*args)
        return wrapped
    return wrap

然后您可以在不同的模块中使用它,如下所示:

from functools32 import lru_cache
from cine import create_if_not_exists_2

@create_if_not_exists_2(globals())
@lru_cache()
def _square(x):
    print "Squaring", x
    return x*x

assert "_square" in globals()
assert "square" in globals()
于 2013-11-25T23:55:58.537 回答
0

在此过程中,我已经对装饰器足够熟悉,因此我很乐意以另一种方式解决问题:

from functools32 import lru_cache

try:
    my_cine
except NameError:
    class my_cine(object):
        _reg_funcs = {}

        @classmethod
        def func_key (cls, f):
            try:
                name = f.func_name
            except AttributeError:
                name = f.__name__
            return (f.__module__, name)

        def __init__(self, f):
            k = self.func_key(f)
            self._f = self._reg_funcs.setdefault(k, f)

        def __call__(self, *args, **kwargs):
            return self._f(*args, **kwargs)


if __name__ == "__main__":
    @my_cine
    @lru_cache()
    def fact_my_cine(n):
        print "In fact_my_cine for", n
        if n < 2:
            return 1
        return fact_my_cine(n-1) * n

    x = fact_my_cine(10)
    print "The answer is", x

@abarnert,如果你还在看,我很想听听你对这种方法的缺点的评价。我知道两个:

  1. 您必须提前知道要查找哪些属性来查找与函数关联的名称。我对它的第一次尝试只查看了 func_name ,它在传递 lru_cache 对象时失败了。
  2. 重置一个函数是痛苦的:del my_cine._reg_funcs[('__main__', 'fact_my_cine')],我在添加 __delitem__ 时所采取的摇摆是不成功的。
于 2013-11-27T01:12:21.590 回答