我在Python中见过并使用过嵌套函数,它们与闭包的定义相匹配。那么为什么它们被称为“嵌套函数”而不是“闭包”呢?

嵌套函数不是闭包,因为它们不被外部使用吗?

更新:我正在阅读关于闭包的文章,它让我想到了关于Python的这个概念。我搜索了下面评论中有人提到的文章,但我不能完全理解文章中的解释,所以我才问这个问题。


当前回答

Python对闭包的支持很弱。要理解我的意思,请参考以下使用JavaScript闭包的计数器示例:

function initCounter(){
    var x = 0;
    function counter  () {
        x += 1;
        console.log(x);
    };
    return counter;
}

count = initCounter();

count(); //Prints 1
count(); //Prints 2
count(); //Prints 3

闭包非常优雅,因为它赋予了这样编写的函数拥有“内部内存”的能力。从Python 2.7开始,这是不可能的。如果你尝试

def initCounter():
    x = 0;
    def counter ():
        x += 1 ##Error, x not defined
        print x
    return counter

count = initCounter();

count(); ##Error
count();
count();

你会得到一个错误,说x没有定义。但如果其他人已经证明你可以打印它,那怎么可能呢?这是因为Python是如何管理函数变量作用域的。内部函数可以读取外部函数的变量,但不能写入它们。

这真是太遗憾了。但是只有只读闭包,你至少可以实现函数装饰器模式,Python为此提供了语法糖。

更新

正如前面所指出的,有很多方法可以处理python的作用域限制,我将介绍一些方法。

1. 使用global关键字(一般不推荐)。

2. 在Python 3中。X,使用nonlocal关键字(建议使用@unutbu和@leewz)

3.定义一个简单的可修改类Object

class Object(object):
    pass

并在initCounter中创建一个Object范围来存储变量

def initCounter ():
    scope = Object()
    scope.x = 0
    def counter():
        scope.x += 1
        print scope.x

    return counter

由于作用域实际上只是一个引用,对其字段所采取的操作并不真正修改作用域本身,因此不会出现错误。

4. @unutbu指出的另一种方法是将每个变量定义为一个数组(x =[0]),并修改它的第一个元素(x[0] += 1)。同样没有出现错误,因为x本身没有被修改。

5. 正如@raxacoricofallapatorius所建议的,你可以让x成为counter的一个属性

def initCounter ():

    def counter():
        counter.x += 1
        print counter.x

    counter.x = 0
    return counter

其他回答

这里提供了一种通过代码对象识别函数是否是闭包的方法。

正如在其他回答中已经提到的,并不是每个嵌套函数都是闭包。给定一个复合函数(表示整个动作),它的中间状态可以是闭包或嵌套函数。 闭包是一种由其(非空的)封闭范围(自由变量空间)“参数化”的函数。注意,复合函数可以由这两种类型组成。

(Python的)内部类型代码 对象表示编译后的函数体。它的属性co_cellvars和co_freevars可用于“查看”函数的闭包/作用域。 正如文件中提到的

Co_freevars:自由变量名的元组(通过函数的闭包引用) Co_cellvars:单元格变量名的元组(由包含作用域引用)。

一旦函数被读取,通过递归调用返回一个局部函数,它带有自己的__closure__(因此是cell_contents)和一个来自它的clousre和作用域的自由变量列表。

让我们介绍一些支持函数

# the "lookarounds"
def free_vars_from_closure_of(f):
    print(f.__name__, 'free vars from its closure',  f.__code__.co_cellvars)

def free_vars_in_scopes_of(f):
    print(f.__name__, 'free vars in its scope    ', f.__code__.co_freevars)

# read cells values
def cell_content(f):
    if f.__closure__ is not None:
        if len(f.__closure__) == 1: # otherwise problem with join
            c = f.__closure__[0].cell_contents
        else:
            c = ','.join(str(c.cell_contents) for c in f.__closure__)
    else:
        c = None

    print(f'cells of {f.__name__}: {c}')

这里有一个例子,来自另一个用更系统的方式重写的答案

def f1(x1):
    def f2(x2):
        a = 'free' # <- better choose different identifier to avoid confusion
        def f3(x3):
            return '%s %s %s %s' %  (x1, x2, a, x3)
        return f3
    return f2

# partial functions
p1 = f1('I')
p2 = p1('am')

# lookaround
for p in (f1, p1, p2):
    free_vars_in_scopes_of(p)
    free_vars_from_closure_of(p)
    cell_content(p)

输出

f1 free vars in its scope     ()         # <- because it's the most outer function
f1 free vars from its closure ('x1',)
cells of f1: None
f2 free vars in its scope     ('x1',)
f2 free vars from its closure ('a', 'x2')
cells of f2: I
f3 free vars in its scope     ('a', 'x1', 'x2')
f3 free vars from its closure ()        # <- because it's the most inner function
cells of f3: free, I, am

对应的lambda:

def g1(x1):
    return lambda x2, a='free': lambda x3: '%s %s %s %s' %  (x1, x2, a, x3)

从自由变量/范围的角度来看是等价的。唯一微小的区别是code对象的某些属性的一些值: Co_varnames, co_consts, co_code, co_lnotab, co_stacksize…当然还有__name__属性。


一个混合的例子,闭包和不立即:

# example: counter
def h1():             # <- not a closure
    c = 0
    def h2(c=c):      # <- not a closure
        def h3(x):    # <- closure
            def h4(): # <- closure
                nonlocal c
                c += 1
                print(c)
            return h4
        return h3
    return h2

# partial functions
p1 = h1()
p2 = p1()
p3 = p2('X')

p1() # do nothing
p2('X') # do nothing
p2('X') # do nothing
p3() # +=1
p3() # +=1
p3() # +=1

# lookaround
for p in (h1, p1, p2, p3):
    free_vars_in_scopes_of(p)
    #free_vars_from_closure_of(p)
    cell_content(p)

输出

1 X
2 X
3 X
h1 free vars in its scope     ()
cells of h1: None
h2 free vars in its scope     ()
cells of h2: None
h3 free vars in its scope     ('c',)
cells of h3: 3
h4 free vars in its scope     ('c', 'x')
cells of h4: 3,X

H1和h2都不是闭包,因为它们在作用域内没有单元格和自由变量。 H3和H3是闭包,共享(在本例中)相同的单元格和c的自由变量。h4有一个带有自己单元格的自由变量x。


最后的考虑:

__closure__属性和__code__。Co_freevars可用于检查自由变量的值和名称(标识符) nonlocal和__code__之间的反类比(在广义上)。Co_cellvars:非局部作用于外部函数__code__。Co_cellvars改为内部函数

人们对什么是终结感到困惑。闭包不是内部函数。封闭的意义是封闭的行为。内部函数封闭在一个非局部变量上,这个变量叫做自由变量。

def counter_in(initial_value=0):
    # initial_value is the free variable
    def inc(increment=1):
        nonlocal initial_value
        initial_value += increment
        print(initial_value)
    return inc

当你调用counter_in()时,它将返回inc函数,该函数有一个自由变量initial_value。所以我们创建了一个CLOSURE。人们称inc为闭包函数,我认为这让人困惑,人们认为"内部函数是闭包"在现实中inc并不是一个闭包,因为它是闭包的一部分,为了方便起见,他们称它为闭包函数。

  myClosingOverFunc=counter_in(2)

返回inc函数,该函数在自由变量initial_value上关闭。当你调用myClosingOverFunc

 myClosingOverFunc() 

它会输出2。

当python看到一个闭包系统存在时,它会创建一个名为CELL的新obj。这将只存储自由变量的名称,在本例中为initial_value。这个Cell obj将指向另一个存储initial_value值的对象。

在我们的例子中,外层函数和内部函数中的initial_value将指向这个单元格对象,而这个单元格对象将指向initial_value的值。

  variable initial_value =====>> CELL ==========>> value of initial_value

所以当你调用counter_in时,它的作用域消失了,但这无关紧要。因为变量initial_value直接引用CELL对象。它间接引用initial_value的值。这就是为什么即使外部函数的作用域消失了,内部函数仍然可以访问自由变量

假设我想写一个函数,它接受一个函数作为参数,并返回这个函数被调用的次数。

def counter(fn):
    # since cnt is a free var, python will create a cell and this cell will point to the value of cnt
    # every time cnt changes, cell will be pointing to the new value
    cnt = 0

    def inner(*args, **kwargs):
        # we cannot modidy cnt with out nonlocal
        nonlocal cnt
        cnt += 1
        print(f'{fn.__name__} has been called {cnt} times')
        # we are calling fn indirectly via the closue inner
        return fn(*args, **kwargs)
    return inner
      

在这个例子中,cnt是我们的自由变量,inner + cnt创建CLOSURE。当python看到这个时,它将创建一个CELL Obj, cnt将总是直接引用这个CELL Obj,而CELL将引用内存中存储cnt值的另一个Obj。最初问= 0。

 cnt   ======>>>>  CELL  =============>  0

当你通过传递参数计数器(myFunc)()调用内部函数时,这将使cnt增加1。因此,我们的引用模式将发生如下变化:

 cnt   ======>>>>  CELL  =============>  1  #first counter(myFunc)()
 cnt   ======>>>>  CELL  =============>  2  #second counter(myFunc)()
 cnt   ======>>>>  CELL  =============>  3  #third counter(myFunc)()

这只是终结的一个例子。您可以通过传递另一个函数来创建闭包的多个实例

counter(differentFunc)()

这将创建一个与上面不同的CELL obj。我们刚刚创建了另一个闭包实例。

 cnt  ======>>  difCELL  ========>  1  #first counter(differentFunc)()
 cnt  ======>>  difCELL  ========>  2  #secon counter(differentFunc)()
 cnt  ======>>  difCELL  ========>  3  #third counter(differentFunc)()


  

Python对闭包的支持很弱。要理解我的意思,请参考以下使用JavaScript闭包的计数器示例:

function initCounter(){
    var x = 0;
    function counter  () {
        x += 1;
        console.log(x);
    };
    return counter;
}

count = initCounter();

count(); //Prints 1
count(); //Prints 2
count(); //Prints 3

闭包非常优雅,因为它赋予了这样编写的函数拥有“内部内存”的能力。从Python 2.7开始,这是不可能的。如果你尝试

def initCounter():
    x = 0;
    def counter ():
        x += 1 ##Error, x not defined
        print x
    return counter

count = initCounter();

count(); ##Error
count();
count();

你会得到一个错误,说x没有定义。但如果其他人已经证明你可以打印它,那怎么可能呢?这是因为Python是如何管理函数变量作用域的。内部函数可以读取外部函数的变量,但不能写入它们。

这真是太遗憾了。但是只有只读闭包,你至少可以实现函数装饰器模式,Python为此提供了语法糖。

更新

正如前面所指出的,有很多方法可以处理python的作用域限制,我将介绍一些方法。

1. 使用global关键字(一般不推荐)。

2. 在Python 3中。X,使用nonlocal关键字(建议使用@unutbu和@leewz)

3.定义一个简单的可修改类Object

class Object(object):
    pass

并在initCounter中创建一个Object范围来存储变量

def initCounter ():
    scope = Object()
    scope.x = 0
    def counter():
        scope.x += 1
        print scope.x

    return counter

由于作用域实际上只是一个引用,对其字段所采取的操作并不真正修改作用域本身,因此不会出现错误。

4. @unutbu指出的另一种方法是将每个变量定义为一个数组(x =[0]),并修改它的第一个元素(x[0] += 1)。同样没有出现错误,因为x本身没有被修改。

5. 正如@raxacoricofallapatorius所建议的,你可以让x成为counter的一个属性

def initCounter ():

    def counter():
        counter.x += 1
        print counter.x

    counter.x = 0
    return counter

《计算机程序的结构和解释》(SICP)的读者:闭包有两种不相关的含义(CS VS Math),后一种含义见维基百科(Wikipedia):

在20世纪80年代,Sussman和Abelson还将闭包一词用于另一个不相关的含义:将数据添加到数据结构中以同时能够添加嵌套数据结构的操作符的属性。该术语的这种用法来自数学用法,而不是先前在计算机科学中的用法。作者认为这种术语上的重叠是“不幸的”。

https://en.wikipedia.org/wiki/Closure_ (computer_programming) # History_and_etymology

第二个(数学)含义也在Python的SICP中使用,参见例如元组的讨论

我们能够将元组用作其他元组的元素,这为我们的编程语言提供了一种新的组合方式。我们把元组以这种方式嵌套的能力称为元组数据类型的闭包属性。通常,如果组合结果本身可以使用相同的方法组合,则组合数据值的方法满足闭包性质。

2.3 Python中的| SICP序列

Python 2没有闭包——它有类似闭包的变通方法。

已经给出的答案中有很多例子——将变量复制到内部函数,在内部函数上修改对象,等等。

在Python 3中,支持更加显式和简洁:

def closure():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

用法:

start = closure()
another = closure() # another instance, with a different stack

start() # prints 1
start() # prints 2

another() # print 1

start() # prints 3

nonlocal关键字将内部函数绑定到显式提到的外部变量,实际上将其封闭起来。因此更明确的是一个“闭包”。