关于闭包:Python 2.x中的非本地关键字

nonlocal keyword in Python 2.x

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


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

要使用维基百科的例子:

1
2
3
4
5
6
7
8
9
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


下面的解决方案是由Elias Zamaria给出的答案所启发的,但与此相反,该答案确实正确地处理了外部函数的多个调用。"变量"inner.y是当前outer调用的本地变量。只有它不是一个变量,因为它是禁止的,而是一个对象属性(对象本身就是函数inner)。这非常难看(请注意,属性只能在定义了inner函数之后创建),但似乎有效。

1
2
3
4
5
6
7
8
9
10
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)


与字典不同的是,非本地类的混乱程度更小。修改@chrisb的示例:

1
2
3
4
5
6
7
def outer():
    class context:
        y = 0
    def inner():
        context.y += 1
        return context.y
    return inner

然后

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

每个outer()调用都创建一个名为context的新的、不同的类(不仅仅是一个新实例)。因此它避免了@nathaniel对共享上下文的注意。

1
2
3
4
5
g = outer()
assert g() == 1
assert g() == 2

assert f() == 5


我认为这里的关键是你所说的"进入"。读取闭包范围之外的变量应该没有问题,例如,

1
2
3
4
5
6
x = 3
def outer():
    def inner():
        print x
    inner()
outer()

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

1
2
3
4
5
6
7
x = 3
def outer():
    def inner():
        x = 5
    inner()
outer()
print x

仍将打印3。根据我对PEP-3104的理解,这就是非本地关键字要涵盖的内容。正如PEP中提到的,您可以使用类来完成相同的事情(有点混乱):

1
2
3
4
5
6
7
8
9
class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x


在python 2中,还有另一种实现非局部变量的方法,以防由于任何原因,这里的任何答案都是不可取的:

1
2
3
4
5
6
7
8
9
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的回答一样。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

更新

最近回顾这一点后,当装饰师意识到将它作为一个整体来实现会使它更通用和有用时(尽管这样做可能会在某种程度上降低它的可读性),我被它是怎样的感觉所震惊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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


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

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

1
2
3
4
5
def outer(v):
    def inner(container = [v]):
        container[0] += 1
        return container[0]
    return inner

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

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

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


将上面的Martineau Elegant解决方案扩展到一个实用且稍微不那么优雅的用例,我得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

使用全局变量

1
2
3
4
5
6
7
8
9
10
11
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答案

1
2
3
4
5
6
7
def report():
        class Rank:
            def __init__(self):
                report.ranks += 1
        rank = Rank()
report.ranks = 0
report()

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