编写要在同一会话中重复执行的脚本是一件很奇怪的事情。
我可以理解您为什么要这样做,但这仍然很奇怪,而且我认为通过看起来有点奇怪来暴露这种奇怪的代码并有解释它的评论是不合理的。
但是,您使事情变得比必要的更丑陋。
首先,您可以这样做:
@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
一个可以导入的模块怎么办?然后它就不起作用了,因为它将引用该模块的全局变量,而不是您当前正在编写的那个。
您可以通过显式传递您的globals
dict、模块对象或模块名称作为参数来解决此问题,例如@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,那么它会起作用。