Python 2.x 中的 nonlocal 关键字

Posted

技术标签:

【中文标题】Python 2.x 中的 nonlocal 关键字【英文标题】:nonlocal keyword in Python 2.x 【发布时间】:2011-03-12 13:36:48 【问题描述】:

我正在尝试在 Python 2.6 中实现一个闭包,我需要访问一个非局部变量,但似乎这个关键字在 python 2.x 中不可用。在这些版本的 python 中,应该如何访问闭包中的非局部变量?

【问题讨论】:

【参考方案1】:

内部函数可以读取 2.x 中的非局部变量,只是不能重新绑定它们。这很烦人,但你可以解决它。只需创建一个字典,并将您的数据作为元素存储在其中。不禁止内部函数变异非局部变量引用的对象。

使用 Wikipedia 中的示例:

def outer():
    d = 'y' : 0
    def inner():
        d['y'] += 1
        return d['y']
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

【讨论】:

为什么可以修改字典中的值? @coelhudo 因为您可以修改非局部变量。但是你不能对非局部变量进行赋值。例如,这将引发 UnboundLocalError:def inner(): print d; d = 'y': 1。在这里,print d 读取外部 d 从而在内部范围内创建非局部变量 d 感谢您的回答。我认为你可以改进术语:而不是“可以阅读,不能改变”,也许是“可以参考,不能分配给”。您可以在非本地范围内更改对象的内容,但不能更改引用的对象。 另外,您可以使用任意类和实例化对象而不是字典。我发现它更加优雅和可读,因为它不会用文字(字典键)淹没代码,而且你可以使用方法。 对于 Python,我认为最好使用动词“bind”或“rebind”,并使用名词“name”而不是“variable”。在 Python 2.x 中,您可以关闭对象,但不能在原始范围内重新绑定名称。在像 C 这样的语言中,声明变量会保留一些存储空间(静态存储空间或堆栈上的临时存储空间)。在 Python 中,表达式 X = 1 只是将名称 X 与特定对象绑定(int 与值 1)。 X = 1; Y = X 将两个名称绑定到同一个对象。无论如何,有些对象是可变的,你可以改变它们的值。【参考方案2】:

以下解决方案的灵感来自answer by Elias Zamaria,但与该答案相反,它确实正确处理了外部函数的多次调用。 “变量”inner.y 是当前调用 outer 的本地变量。只是它不是一个变量,因为这是被禁止的,而是一个对象属性(对象是函数inner 本身)。这很丑(注意属性只能在inner函数定义后创建)但是看起来很有效。

def outer():
    def inner():
        inner.y += 1
        return inner.y
    inner.y = 0
    return inner

f = outer()
g = outer()
print(f(), f(), g(), f(), g()) #prints (1, 2, 1, 3, 2)

【讨论】:

它必须不丑。不要使用inner.y,而是使用outer.y。你可以在 def inner 之前定义 outer.y = 0。 好的,我看到我的评论是错误的。 Elias Zamaria 也实现了 outer.y 解决方案。但正如 Nathaniel [1] 所评论的,您应该小心。我认为这个答案应该被提升为解决方案,但请注意 outer.y 解决方案和注意事项。 [1]***.com/questions/3190706/… 如果你想要多个内部函数共享非局部可变状态,这会变得很丑。假设从外部返回的 inc()dec() 递增和递减共享计数器。然后,您必须决定将当前计数器值附加到哪个函数并从其他函数引用该函数。这看起来有些奇怪和不对称。例如。在dec()inc.value -= 1 这样的一行。【参考方案3】:

非本地类比字典更简洁。修改@ChrisB的example:

def outer():
    class context:
        y = 0
    def inner():
        context.y += 1
        return context.y
    return inner

然后

f = outer()
assert f() == 1
assert f() == 2
assert f() == 3
assert f() == 4

每个 external() 调用都会创建一个新的、不同的类,称为 context(不仅仅是一个新实例)。所以它避免了@Nathaniel's beware关于共享上下文。

g = outer()
assert g() == 1
assert g() == 2

assert f() == 5

【讨论】:

不错!这确实比字典更优雅、更易读。 我建议在这里一般使用插槽。它可以保护您免受拼写错误。 @DerWeh 很有趣,我已经 never heard 了。你能对任何班级说同样的话吗?我确实讨厌被拼写错误弄得一头雾水。 @BobStein 抱歉,我不太明白你的意思。但是通过添加__slots__ = () 并创建一个对象而不是使用该类,例如context.z = 3 会引发 AttributeError。所有类都可以,除非它们继承自未定义槽的类。 @DerWeh 我从未听说过使用 slots 来捕捉错别字。你是对的,它只会在类实例中有所帮助,而不是类变量。也许您想在 pyfiddle 上创建一个工作示例。这将需要至少两条额外的线路。无法想象如果没有更多混乱,您将如何做到这一点。【参考方案4】:

我认为这里的关键是您所说的“访问”。读取闭包范围之外的变量应该没有问题,例如,

x = 3
def outer():
    def inner():
        print x
    inner()
outer()

应该按预期工作(打印 3)。但是,覆盖 x 的值不起作用,例如,

x = 3
def outer():
    def inner():
        x = 5
    inner()
outer()
print x

仍会打印 3。根据我对PEP-3104 的理解,这就是 nonlocal 关键字的含义。正如 PEP 中提到的,你可以使用一个类来完成同样的事情(有点乱):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x

【讨论】:

可以简单地创建一个函数,而不是创建一个类并实例化它:def ns(): pass,后跟ns.x = 3。它不漂亮,但在我看来它稍微不那么难看。 投反对票有什么特别的原因吗?我承认我的不是最优雅的解决方案,但它确实有效...... class Namespace: x = 3 呢? 我不认为这种方式模拟了非本地,因为它创建了一个全局引用而不是闭包中的引用。 我同意 @CarmeloS,你在 not 中的最后一个代码块按照 PEP-3104 建议的方式在 Python 2 中完成类似的事情。ns 是一个全球性的这就是为什么您可以在最后的 print 语句中在模块级别引用 ns.x 的原因。【参考方案5】:

还有另一种在 Python 2 中实现非局部变量的方法,以防这里的任何答案由于某种原因不受欢迎:

def outer():
    outer.y = 0
    def inner():
        outer.y += 1
        return outer.y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

在变量的赋值语句中使用函数的名称是多余的,但在我看来它比将变量放入字典中更简单、更清晰。就像在 Chris B. 的回答中一样,从一个呼叫到另一个呼叫都会记住该值。

【讨论】:

请注意:以这种方式实现时,如果您先执行f = outer(),然后再执行g = outer(),则f 的计数器将被重置。这是因为它们都共享一个 single outer.y 变量,而不是每个都有自己独立的变量。尽管这段代码看起来比 Chris B 的答案更美观,但如果您想多次调用 outer,他的方式似乎是模拟词法作用域的唯一方法。 @Nathaniel:让我看看我是否理解正确。对outer.y 的赋值不涉及函数调用(实例)outer() 的任何本地内容,而是分配给在其封闭 中绑定到名称outer 的函数对象的属性范围。因此,同样可以在写outer.y 时使用任何其他 名称来代替outer,前提是已知它受该范围的约束。这是正确的吗? 更正,我应该说,在“绑定在那个范围内”之后:到一个类型允许设置属性的对象(如函数或任何类实例)。此外,由于这个范围实际上比我们想要的范围更远,这是否会建议以下极其丑陋的解决方案:而不是 outer.y 使用名称 inner.y (因为 inner 绑定在调用 outer() 内,这正是我们想要的范围),但是将初始化 inner.y = 0 放在 inner 的定义之后(因为对象在其属性创建时必须存在),但是当然在return inner之前? @MarcvanLeeuwen 看起来您的评论启发了 Elias 的 inner.y 解决方案。你的想法很好。 访问和更新全局变量可能不是大多数人在改变闭包捕获时尝试做的事情。【参考方案6】:

以下内容的灵感来自 Alois Mahdal 在 comment 中提出的关于另一个 answer 的建议:

class Nonlocal(object):
    """ Helper to implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


def outer():
    nl = Nonlocal(y=0)
    def inner():
        nl.y += 1
        return nl.y
    return inner

f = outer()
print(f(), f(), f()) # -> (1 2 3)

【讨论】:

就可读性和保留词法范围而言,这似乎是最好的。 如果您不止一次调用outer(),您的第二种解决方案将不起作用,因为所有调用共享同一个计数器。这与使用全局变量本质上没有什么不同。 @interjay:是的,因为函数属性实际上是全局变量(因为函数本身就是)。要修复它,需要一种方法来重置函数属性的值——这是可行的。 重置该值仍然不够好,因为它会阻止您并行使用两个计数器。我只想删除这部分答案。【参考方案7】:

python 的作用域规则有一个缺点——赋值使变量成为其直接封闭函数作用域的局部变量。对于全局变量,您可以使用 global 关键字解决此问题。

解决方案是引入一个在两个作用域之间共享的对象,该对象包含可变变量,但本身通过未分配的变量引用。

def outer(v):
    def inner(container = [v]):
        container[0] += 1
        return container[0]
    return inner

另一种选择是一些范围黑客:

def outer(v):
    def inner(varname = 'v', scope = locals()):
        scope[varname] += 1
        return scope[varname]
    return inner

您可能会想出一些技巧来将参数的名称传递给outer,然后将其作为varname 传递,但不依赖名称outer,您需要使用Y 组合器。

【讨论】:

这两个版本都适用于这种特殊情况,但不能替代nonlocallocals() 在定义 inner() 时创建一个包含 outer()s 本地人的字典,但更改该字典不会更改 outer() 中的 v。当您有更多想要共享封闭变量的内部函数时,这将不再起作用。说一个 inc()dec() 来增加和减少一个共享计数器。 @BlackJack nonlocal 是 python 3 的一个特性。 是的,我知道。问题是在Python 2一般中如何实现Python 3 的nonlocal 的效果。您的想法不涵盖一般情况,而仅涵盖具有 one 内部功能的情况。以this gist 为例。两个内部函数都有自己的容器。正如其他答案已经建议的那样,您需要一个外部函数范围内的可变对象。 @BlackJack 这不是问题,但感谢您的意见。 是的,的问题。看看页面顶部。 adinsa 有 Python 2.6,需要在没有 Python 3 中引入的 nonlocal 关键字的情况下访问闭包中的非局部变量。【参考方案8】:

另一种方法(虽然它太冗长):

import ctypes

def outer():
    y = 0
    def inner():
        ctypes.pythonapi.PyCell_Set(id(inner.func_closure[0]), id(y + 1))
        return y
    return inner

x = outer()
x()
>> 1
x()
>> 2
y = outer()
y()
>> 1
x()
>> 3

【讨论】:

其实我更喜欢使用字典的解决方案,但是这个很酷:)【参考方案9】:

将上面的 Martineau 优雅解决方案扩展到我得到的一个实用且不太优雅的用例:

class nonlocals(object):
""" Helper to implement nonlocal names in Python 2.x.
Usage example:
def outer():
     nl = nonlocals( n=0, m=1 )
     def inner():
         nl.n += 1
     inner() # will increment nl.n

or...
    sums = nonlocals(  k:v for k,v in locals().iteritems() if k.startswith('tot_')  )
"""
def __init__(self, **kwargs):
    self.__dict__.update(kwargs)

def __init__(self, a_dict):
    self.__dict__.update(a_dict)

【讨论】:

【参考方案10】:

使用全局变量

def outer():
    global y # import1
    y = 0
    def inner():
        global y # import2 - requires import1
        y += 1
        return y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

就个人而言,我不喜欢全局变量。但是,我的建议是基于https://***.com/a/19877437/1083704 答案

def report():
        class Rank: 
            def __init__(self):
                report.ranks += 1
        rank = Rank()
report.ranks = 0
report()

这里用户需要声明一个全局变量ranks,每次需要调用report。我的改进消除了从用户初始化函数变量的需要。

【讨论】:

使用字典要好得多。您可以引用inner 中的实例,但不能分配给它,但您可以修改它的键和值。这避免了使用全局变量。

以上是关于Python 2.x 中的 nonlocal 关键字的主要内容,如果未能解决你的问题,请参考以下文章

python中的关键字global和nonlocal

Python中的闭包global关键字nonlocal关键字和装饰器

Python 关键字之 nonlocal

python中global 和 nonlocal 的作用域

Python迭代器和关键字 global ,nonlocal

python3raise,assert,nonlocal 关键字解读