可以用上下文管理器模拟词法作用域吗?
在之前的一篇帖子中,我提到过如何避免在一些模式中使用中间变量 tmp
:
tmp = <some operation>
result = tmp[<boolean expression>]
del tmp
...这里的 tmp
是一个 pandas 对象。例如:
tmp = df.xs('A')['II'] - df.xs('B')['II']
result = tmp[tmp < 0]
del tmp
我对这种模式的执念,主要是因为我一直渴望能有真正的词法作用域1,即使在编写 Python 多年后,这种想法依然挥之不去。在 Python 中2,我只能通过明确调用 del
来应对。
我突然想到,或许可以使用上下文管理器来模拟 Python 中的词法作用域。它的样子可能是这样的:
with my(df.xs('A')['II'] - df.xs('B')['II']) as tmp:
result = tmp[tmp < 0]
为了能够模拟词法作用域,上下文管理器类需要有一种方法来 del
删除调用作用域中被赋值为其(上下文管理器的)enter 方法返回值的变量。
例如,稍微作弊一下:
import contextlib as cl
# herein lies the rub...
def deletelexical():
try: del globals()['h']
except: pass
@cl.contextmanager
def my(obj):
try: yield obj
finally: deletelexical()
with my(2+2) as h:
print h
try:
print h
except NameError, e:
print '%s: %s' % (type(e).__name__, e)
# 4
# Name error: name 'h' is not defined
当然,问题在于如何真正实现 deletelexical
。这能做到吗?
编辑:正如 abarnert 指出的,如果在周围的作用域中已经存在一个 tmp
,那么 deletelexical
并不会恢复它,因此这几乎不能算作词法作用域的模拟。正确的实现必须保存周围作用域中任何现有的 tmp
变量,并在 with 语句结束时将它们替换回来。
1例如,在 Perl 中,我会用类似这样的代码:
my $result = do {
my $tmp = $df->xs('A')['II'] - $df->xs('B')['II'];
$tmp[$tmp < 0]
};
或者在 JavaScript 中:
var result = function () {
var tmp = df.xs('A')['II'] - df.xs('B')['II'];
return tmp[tmp < 0];
}();
编辑:针对 abarnert 的帖子和评论:是的,在 Python 中可以定义
def tmpfn():
tmp = df.xs('A')['II'] - df.xs('B')['II']
return tmp[tmp < 0]
...这样确实可以避免用无用的名称 tmp
来污染命名空间,但这样做又会用无用的名称 tmpfn
来污染命名空间。JavaScript(还有 Perl 等其他语言)允许使用匿名函数,而 Python 不支持。无论如何,我认为 JavaScript 的匿名函数是实现词法作用域的一种相对繁琐的方法;虽然比没有好,且我经常使用,但远不如 Perl 的方式(我不仅指 Perl 的 do
语句,还包括它提供的其他控制作用域的方法,无论是词法的还是动态的)。
2我不需要被提醒,只有极少数的 Python 程序员会在意词法作用域。
1 个回答
在你的JavaScript代码中,你这样做:
var result = function () {
var tmp = df.xs('A')['II'] - df.xs('B')['II'];
return tmp[tmp < 0];
}();
换句话说,为了获得一个额外的作用域,你创建了一个新的本地函数,并使用它的作用域。在Python中,你也可以做到这一点:
def tmpf():
tmp = df.xs('A')['II'] - df.xs('B')['II']
return tmp[tmp < 0]
result = tmpf()
而且效果完全一样。
不过,这个效果可能不是你想的那样。离开作用域只是意味着那些东西可以被垃圾回收了。真正的作用域会给你这个效果,但这并不是你想要的(你想要的是在某个时刻确定性地销毁某个东西)。没错,在CPython 2.7中,这通常会达到你想要的效果,但这并不是语言的特性,而是实现的细节。
而且你的想法在使用函数的问题上又增加了一些新的问题。
你的想法会改变在with
语句中定义或重新绑定的所有内容。而JavaScript的对应做法并不会这样。你说的更像是C++中的作用域保护宏,而不是let
语句。(一些不太纯粹的语言允许你在let
中set!
绑定新的名字,这些名字会在let
之外继续存在,你可以把这描述为一个隐含的nonlocal everything-but-the-let-names
的作用域,但这依然很奇怪。尤其是在一个已经对重新绑定和变更有明确区分的语言中。)
另外,如果你已经有一个全局变量叫tmp
,这个with
语句会把它覆盖掉。这不是let
语句或其他常见的作用域方式所做的。(如果tmp
是一个局部变量而不是全局变量呢?)
如果你想用上下文管理器来模拟作用域,你真正需要的是一个在退出时能恢复globals
和/或locals
的上下文管理器。或者,可能只是一个在临时globals
和/或locals
中执行任意代码的方法。(我不确定这是否可能,但你明白我的意思——就像把with
的主体作为code
对象获取并传递给exec
。)
或者,如果你想允许重新绑定但不允许新绑定,可以遍历globals
和/或locals
,删除所有新的绑定。
或者,如果你只想删除某个特定的东西,可以写一个deleting
上下文管理器:
with deleting('tmp'):
tmp = df.xs('A')['II'] - df.xs('B')['II']
result = tmp[tmp < 0]
没有必要把表达式放进with
语句中,然后试图搞清楚它绑定到了什么。