在内部函数中分配外部变量的python闭包

python closure with assigning outer variable inside inner function

我有这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python

def get_match():
  cache=[]
  def match(v):
    if cache:
      return cache
    cache=[v]
    return cache
  return match
m = get_match()
m(1)

如果我运行它,它会说:

1
UnboundLocalError: local variable 'cache' referenced before assignment

但如果我这样做:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python

def get():
  y = 1
  def m(v):
    return y + v
  return m

a=get()
a(1)

它运行。

有清单吗?或者我的代码组织错误?


问题是变量cache不在函数匹配的范围内。如果您只想像在第二个示例中那样读取它,那么这不是一个问题,但是如果您要给它赋值,Python会将它解释为一个局部变量。如果您使用的是python 3,那么您可以使用nonlocal关键字来解决这个问题——不幸的是,对于python 2没有简单的解决方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def f():
    v = 0

    def x():
        return v    #works because v is read from the outer scope

    def y():
        if v == 0:  #fails because the variable v is assigned to below
            v = 1

    #for python3:
    def z():
        nonlocal v  #tell python to search for v in the surrounding scope(s)
        if v == 0:
            v = 1   #works because you declared the variable as nonlocal

全局变量的问题有点相同——每次分配给全局变量时都需要使用global,但不是为了读取它。

对其背后原因的简短解释:python解释器将所有函数编译成function类型的特殊对象。在编译期间,它检查函数创建的所有局部变量(用于垃圾收集等)。这些变量名保存在函数对象中。由于"隐藏"外部作用域变量(创建同名变量)是完全合法的,因此任何分配给且未显式声明为EDOCX1(或python3中的nonlocal)的变量都被假定为局部变量。

当函数执行时,解释器必须查找它遇到的每个变量引用。如果在编译期间发现变量是本地变量,则在函数f_locals字典中进行搜索。如果尚未将其分配给,则会引发遇到的异常。如果变量未在函数作用域中赋值,因而不是其局部变量的一部分,则会在周围作用域中查找该变量-如果在那里找不到该变量,则会引发类似的异常。


访问变量与分配变量不同。

全局变量的情况类似。您可以在任何函数中访问它们,但是如果您试图在不使用global语句的情况下分配给它,它将在本地上下文中重新声明它。

不幸的是,对于本地函数,没有等效的global语句,但是您可以通过替换来绕过重新声明。

1
cache=[v]

用:

1
cache[:] = [v]


由于python看到cache=[v]赋值给cache,所以它将其视为局部变量。所以这个错误是相当合理的——在if语句中使用之前没有定义局部变量cache

您可能想将其写为:

1
2
3
4
5
6
7
8
9
10
def get_match():
  cache=[]
  def match(v):
    if cache:
      return cache
    cache.append(v)
    return cache
  return match
m = get_match()
m(1)

强烈建议阅读:执行模型-命名和绑定和PEP 227-静态嵌套范围


替换

1
2
cache=[]
def match(v):

具有

1
def match(v,cache=[])

说明:您的代码将EDCOX1 OR 0作为EDCOX1 OR 13的变量声明,EDCX1 OR 14引用的变量不知道(由于以下赋值)。但是,您确实希望EDCOX1×0是EDCOX1·16的命名空间的一部分。

我知道这样一个"恶意"用户可以重新定义缓存,但这是他们自己的错误。如果这是一个问题,则备选方案是:

1
2
3
4
5
6
7
def match(v):
     try:
         if cache:
             return cache
     except NameError:
         cache = []
     ...

(见这里)