可以用上下文管理器模拟词法作用域吗?

3 投票
1 回答
1009 浏览
提问于 2025-04-17 16:32

在之前的一篇帖子中,我提到过如何避免在一些模式中使用中间变量 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 个回答

3

在你的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语句。(一些不太纯粹的语言允许你在letset!绑定新的名字,这些名字会在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语句中,然后试图搞清楚它绑定到了什么。

撰写回答