【问题标题】:Can one mimic lexical scoping with context managers?可以用上下文管理器模拟词汇范围吗?
【发布时间】:2013-02-21 06:28:49
【问题描述】:

earlier post 中,我询问了在以下模式中避免中间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 编程,它也不会消亡。在 Python2 中,我使用对 del 的显式调用,

在我看来,可能可以使用上下文管理器来模拟 Python 中的词法作用域。它看起来像这样:

with my(df.xs('A')['II'] - df.xs('B')['II']) as tmp:
    result = tmp[tmp < 0]

为了能够模拟词法作用域,上下文管理器类需要有一种方法来delete 调用作用域中的变量,该变量被分配了由其(上下文管理器的)返回的值 '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 指出的那样,如果在周围的范围内已经存在一个 tmpdeletelexical 将不会恢复它,因此它几乎不能被视为词法范围的模拟。正确的实现必须将任何现有的 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 程序员对词法作用域嗤之以鼻。

【问题讨论】:

  • 我认为你做不到,至少如果不深入到 hack-and-implementation-details 领域,那是不值得的。顺便说一句,Python 做了词法作用域,你只需要一种更细粒度的方式来引入新的作用域。
  • 首先,在 JS 中,你只是定义了一个本地函数并调用它,它允许你使用本地函数的作用域。你可以在 Python 中做同样的事情。那么,这有什么问题呢?你反对给函数命名吗?
  • 第二,你展示了一个 perl 和 JS 语法的例子,它释放了 within 范围内定义的所有内容,然后尝试构建 Python 语法,让所有定义在范围内单独,但释放范围之外定义的特定事物。它更像是一个 let 语句,其中包含一个隐式的 nonlocal everything-else,或者一个 C++ 范围保护宏,而不是一个真正的词法范围。这真的是你想要的吗? (另外,如果您在此 with 语句之前已经有一个名为 tmp 的全局变量会发生什么?如果您有一个名为 tmplocal 会发生什么?)
  • 这似乎是一个刚刚恢复 globals 和/或 locals 的上下文管理器(或者可能只是从其中一个/两个中删除了任何新定义的名称,同时允许已经存在但反弹保持反弹的名字?)更适合您的要求,并且可能没有那么难。或者,更简单地说,一种使用临时globals 和/或locals(空、从周围复制或显式传入)执行一些任意代码的方法?

标签: 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 语句中的所有定义或反弹都发生了变化。 JS 等价物不这样做。您所说的更像是 C++ 范围保护宏,而不是 let 语句。 (一些不纯的语言允许您在 let 中绑定新名称 let,这些新名称将存在于 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 语句并尝试找出它绑定到什么。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-18
    • 2022-01-22
    • 1970-01-01
    • 1970-01-01
    • 2018-11-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多