Python 2.x中的nonlocal关键字


117

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

Answers:


125

内部函数可以读取 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

4
为什么可以从字典中修改值?
coelhudo 2012年

9
@coelhudo因为可以修改非局部变量。但是您不能对非局部变量进行赋值。例如,这将提高UnboundLocalError: def inner(): print d; d = {'y': 1}。在这里,print d读取外部,d从而d在内部范围内创建非局部变量。
suzanshakya'5年

21
感谢您的回答。我认为您可以改善术语:而不是“可以阅读,不能更改”,而可以是“可以引用,不能分配给”。您可以在非本地范围内更改对象的内容,但不能更改引用哪个对象。
metamatt

3
另外,您可以使用任意类和实例化对象代替字典。我发现它更加优雅和易读,因为它不会用文字(字典键)填充代码,而且您可以使用方法。
Alois Mahdal

6
对于Python,我认为最好使用动词“ bind”或“ rebind”并使用名词“ name”而不是“ variable”。在Python 2.x中,您可以关闭对象,但不能在原始范围内重新绑定名称。在像C这样的语言中,声明变量会保留一些存储(静态存储或堆栈上的临时存储)。在Python中,表达式X = 1只是将名称X与特定对象(int带有值1)绑定在一起。 X = 1; Y = X将两个名称绑定到相同的确切对象。无论如何,有些对象是可变的,您可以更改它们的值。
steveha

37

以下解决方案的灵感来自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内部之前定义external.y = 0。
jgomo3 2013年

1
好的,我看到我的评论是错误的。Elias Zamaria也实施了外部解决方案。但是正如Nathaniel [1]所评论的那样,您应该提防。我认为应该将这个答案作为解决方案,但要注意外部解决方案和注意事项。[1] stackoverflow.com/questions/3190706/...
jgomo3

如果您想要多个内部函数共享非局部可变状态,这将变得很丑陋。说一个inc()和一个dec()从外部返回的值,表示增加和减少共享计数器。然后,您必须确定将当前计数器值附加到哪个函数,并从另一个函数引用该函数。看起来有些奇怪且不对称。例如dec()一行inc.value -= 1
BlackJack

33

与字典相比,非本地类的混乱程度更低。修改@ChrisB的示例

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()调用都会创建一个称为上下文的新的独特类(不仅仅是一个新实例)。因此,它避免了@Nathaniel提防共享上下文。

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

assert f() == 5

真好!这确实比字典更优雅,更易读。
温琴佐2015年

我建议在这里一般使用插槽。它可以保护您免受错别字的困扰。
DerWeh's

@DerWeh有趣,我从未听说过。您能在任何班级说相同的话吗?我确实讨厌被错别字弄乱了。
鲍勃·斯坦

@BobStein抱歉,我不太明白您的意思。但是通过添加__slots__ = ()和创建对象而不是使用类,例如context.z = 3会引发一个AttributeError。所有类都是可能的,除非它们从未定义插槽的类继承。
DerWeh

@DerWeh我从未听说过使用插槽捕捉拼写错误。没错,它仅对类实例有用,对类变量没有帮助。也许您想在pyfiddle上创建一个有效的示例。至少需要两条额外的线。无法想象没有更多混乱的情况。
鲍勃·斯坦

14

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

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。它虽然不漂亮,但在我看来却不那么难看。
davidchambers,

2
否决票有任何特殊原因吗?我承认我的解决方案不是最优雅的解决方案,但它可以起作用...
ig0774 2013年

2
class Namespace: x = 3
Feuermurmel

3
我不认为这种方式会模拟非本地,因为它会创建全局引用而不是闭包中的引用。
CarmeloS 2014年

1
我同意@CarmeloS,您的最后代码段没有执行PEP-3104所建议的在Python 2中完成类似操作的方式。它ns是一个全局对象,这就是为什么您可以ns.xprint语句末尾在模块级别引用的原因。
martineau

13

如果由于任何原因,此处的任何答案都不理想,则可以使用另一种方法在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

在变量的赋值语句中使用函数名称是多余的,但对我来说,比将变量放入字典中看起来更简单和简洁。就像克里斯·B(Chris B.)的回答一样,一个电话会记住另一个电话的价值。


19
请注意:以这种方式实施时,如果您先执行f = outer(),然后再执行g = outer(),则f的计数器将被重置。这是因为他们都共享一个单一 outer.y变量,而不是每个都具有自己独立的一个。尽管此代码看起来比Chris B的回答更具美感,但如果您想outer多次调用,他的方法似乎是模仿词汇作用域的唯一方法。
纳撒尼尔(Nathaniel)2013年

@Nathaniel:让我看看我是否正确理解这一点。对的分配outer.y不涉及函数调用(实例)本地的任何内容outer(),而是分配给函数对象的属性,该属性绑定到outer包围范围中的名称。因此,一个同样可以使用了,以书面形式outer.y任何其他名称代替outer,只要它是已知在范围进行约束。这样对吗?
Marc van Leeuwen 2013年

1
我应该说过“在该范围内”之后的更正:对一个其类型允许设置属性的对象(例如函数或任何类实例)。另外,由于此作用域实际上比我们想要的作用域更远,因此这不建议使用以下极为丑陋的解决方案:而不outer.y使用名称inner.y(因为inner绑定在call内outer(),这正是我们想要的作用域),但是将在内部定义inner.y = 0 之后的初始化(因为对象在创建其属性时必须存在),但是当然在?return inner
Marc van Leeuwen 2013年

@MarcvanLeeuwen看来您的评论通过Elias的inner.y启发了解决方案。您的好主意。
Ankur Agarwal

大多数人在对闭包的捕获进行突变时都不会尝试访问和更新全局变量。
binki 2015年

12

以下是Alois Mahdal在评论另一个答案时提出的建议:

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)

更新资料

在最近回顾了这一点之后,我对它的装饰风格感到震惊-当我想到将其实现为装饰器将使它更加通用和有用时(尽管这样做无疑会在某种程度上降低其可读性)。

# Implemented as a decorator.

class Nonlocal(object):
    """ Decorator class to help implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self._vars = kwargs

    def __call__(self, func):
        for k, v in self._vars.items():
            setattr(func, k, v)
        return func


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


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

请注意,这两个版本均可在Python 2和3中使用。


就可读性和保留词汇范围而言,这似乎是最好的。
亚伦·库兰

3

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()创造了一本字典outer()在时间s当地人inner()的定义,但改变这种字典里不会改变vouter()。当您有更多内部函数想要共享一个封闭的over变量时,这将不再起作用。说一个inc()dec()然后递增和递减一个共享计数器。
BlackJack

@BlackJack nonlocal是python 3功能。
Marcin

是的我知道。问题是一般如何nonlocal在Python 2 实现Python 3的效果。您的想法不涉及一般情况,而仅涉及具有一个内部功能的情况。看看这个要点作为例子。这两个内部函数都有其自己的容器。正如其他答案所建议的那样,您需要在外部函数范围内使用可变对象。
BlackJack

@BlackJack这不是问题,但感谢您的投入。
Marcin

是的,这就是问题。看一下页面顶部。adinsa有Python 2.6中,需要访问非本地变量在一个封闭没有nonlocal在Python 3引入了关键字
大酒杯

3

另一种方法(尽管太冗长了):

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

1
实际上,我更喜欢使用字典的解决方案,但这很酷:)
Ezer Fernandes 2014年

0

将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)

-3

使用全局变量

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://stackoverflow.com/a/19877437/1083704回答

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

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


使用字典会更好。您可以在中引用实例inner,但不能为其分配实例,但是可以修改其键和值。这样可以避免使用全局变量。
johannestaas 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.