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

2024-02-14

我经常在 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

但是当函数很长时,将其定义放在 a 中会感觉很奇怪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

?


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

我明白你为什么想要这样做,但它仍然很奇怪,而且我认为代码通过看起来有点奇怪并有注释来解释它来暴露这种奇怪之处并不是不合理的。

然而,你让事情变得比必要的更加丑陋。

首先,你可以这样做:

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

try:
    safe_square_2
except NameError:
    safe_square_2 = _square

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


There is递归函数的潜在问题。它已经是您工作方式所固有的,并且缓存不会以任何方式添加到它,但您可能只会notice这是因为缓存的原因,所以我将解释它并展示如何修复它。考虑这个函数:

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

当您重新执行脚本时,即使您引用了旧的脚本_fact,最终会调用新的_fact,因为它正在访问_fact作为全球名称。这与@lru_cache;删除它,旧函数将still最终调用新的_fact.

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

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

现在老了_fact将会通知fact,仍然是旧的_fact。同样,无论有没有缓存装饰器,它的工作原理都是相同的。


除了最初的技巧之外,您还可以将整个模式分解为一个简单的装饰器。下面我会一步步解释,或者看这篇博文 http://stupidpythonideas.blogspot.com/2013/11/if-not-exists-definitions.html.


不管怎样,即使有了不太丑陋的版本,它仍然有点丑陋和冗长。如果你这样做了几十次,我的“好吧,它should look a bit“丑陋”的理由很快就会消失。因此,您需要以始终排除丑陋的方式来处理此问题:将其包装在函数中。

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

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

The globals函数只返回当前作用域的全局字典。这是一个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 that 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__),这样它就可以找到你的全局变量而不是它的全局变量。但这是丑陋且重复的。

您还可以使用丑陋的框架 hack 来修复它。至少在 CPython 中,sys._getframe() http://docs.python.org/3/library/sys.html#sys._getframe可用于获取呼叫者的帧,并且frame objects http://docs.python.org/3/library/inspect.html#types-and-members有对其全局名称空间的引用,因此:

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

记住一个函数,以便当我在 Python 中重新运行该文件时它不会被重置 的相关文章

随机推荐