记住一个函数,以便在我用Python重新运行文件时不会重置它

kuz*_*roo 2 python decorator memoization lru functools

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

如果我写:

import functools32

@functools32.lru_cache()
def square(x):
    print "Squaring", x
    return x*x
Run Code Online (Sandbox Code Playgroud)

我得到这个行为:

>>> square(10)
Squaring 10
100
>>> square(10)
100
>>> runfile(...)
>>> square(10)
Squaring 10
100
Run Code Online (Sandbox Code Playgroud)

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

try:
    safe_square
except NameError:
    @functools32.lru_cache()
    def safe_square(x):
        print "Squaring", x
        return x*x
Run Code Online (Sandbox Code Playgroud)

但是当函数很长时,将它的定义放在一个try块中感觉很奇怪.我可以这样做:

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

try:
    safe_square_2
except NameError:
    safe_square_2 = functools32.lru_cache()(_square)
Run Code Online (Sandbox Code Playgroud)

但感觉非常人为(例如,在没有'@'符号的情况下调用装饰器)

是否有一种简单的方法来处理这个问题,例如:

@non_resetting_lru_cache()
def square(x):
    print "Squaring", x
    return x*x
Run Code Online (Sandbox Code Playgroud)

aba*_*ert 7

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

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

但是,你做的事情比必要的更糟糕.

首先,你可以这样做:

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

try:
    safe_square_2
except NameError:
    safe_square_2 = _square
Run Code Online (Sandbox Code Playgroud)

将缓存附加到新_square定义没有任何害处.它不会浪费任何时间,也不会浪费超过几个字节的存储空间,最重要的是,它不会影响先前 _square定义的缓存.这就是关闭的重点.


还有就是在这里与递归函数的潜在问题.它已经固有的工作方式,并且缓存不会以任何方式添加它,但你可能只会因为缓存而注意到它,所以我将解释它并展示如何修复它.考虑这个功能:

@lru_cache()
def _fact(n):
    if n < 2:
        return 1
    return _fact(n-1) * n
Run Code Online (Sandbox Code Playgroud)

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

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

@lru_cache()
def _fact(n):
    if n < 2:
        return 1
    return fact(n-1) * n
Run Code Online (Sandbox Code Playgroud)

现在老人_fact会打电话fact,这仍然是旧的_fact.同样,无论有没有缓存装饰器,它的工作方式都相同.


除了最初的技巧,你可以将整个模式分解为一个简单的装饰器.我将在下面逐步解释,或者看看这篇博客文章.


无论如何,即使是不那么丑陋的版本,它仍然有点丑陋和冗长.如果你这样做了几十次,我的"好吧,看起来应该看起来有点难看",理由会非常快.因此,您将要像处理丑陋一样处理此问题:将其包装在函数中.

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

globals().setdefault('fact', _fact)
Run Code Online (Sandbox Code Playgroud)

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

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

def new_bind(name, value):
    globals().setdefault(name, value)
Run Code Online (Sandbox Code Playgroud)

...你可以把它变成一个装饰器几乎是琐碎的:

def new_bind(name):
    def wrap(func):
        globals().setdefault(name, func)
        return func
    return wrap
Run Code Online (Sandbox Code Playgroud)

您可以这样使用:

@new_bind('foo')
def _foo():
    print(1)
Run Code Online (Sandbox Code Playgroud)

但等等,还有更多!在funcnew_bind得到的是将有一个__name__吧?如果您坚持使用命名约定,例如"私有"名称必须是带有_前缀的"公共"名称,我们可以这样做:

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

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

@new_bind
@lru_cache()
def _square(x):
    print "Squaring", x
    return x*x
Run Code Online (Sandbox Code Playgroud)

有一个小问题:如果你使用任何其他没有正确包装函数的装饰器,它们将破坏你的命名约定.所以...就是不要这样做.:)


而且我认为这在每个边缘情况下都能按照你想要的方式运行.特别是,如果您已经编辑了源并想要使用新缓存强制新定义,那么您只需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
Run Code Online (Sandbox Code Playgroud)

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


aba*_*ert 5

persistent_lru_cachestdlib中没有.但你可以很容易地建立一个.

所述functools是直接从链接的文档,因为这是这是有用,因为样本代码,因为它是直接使用它的那些模块中的一个.

如您所见,缓存只是一个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.
Run Code Online (Sandbox Code Playgroud)

当然,只有你的参数是字符串才有效.它可能有点慢.

因此,您可能希望将其保留为内存中dict,并且只编写将其pickle到文件中的代码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
Run Code Online (Sandbox Code Playgroud)

或者,如果您希望它写入每N个新值(因此您不会丢失整个缓存,例如,一个_exit或一个段错误或某人拉绳子),请将其添加到第二个和第三个版本wrapper,紧接着misses += 1:

            if misses % N == 0:
                cache_save()
Run Code Online (Sandbox Code Playgroud)

请参阅此处查看到目前为止的所有工作版本(使用save_every"N"参数,并默认为1,您可能在现实生活中不需要).

如果你想要非常聪明,可以复制缓存并将其保存在后台线程中.

您可能希望扩展cache_info以包括诸如缓存写入次数,自上次缓存写入以来的未命中次数,启动时缓存中的条目数,...

并且可能还有其他方法可以改善这一点.

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