为什么Python嵌套函数不叫闭包?

281 投票
10 回答
112218 浏览
提问于 2025-04-16 06:05

我见过并使用过Python中的嵌套函数,它们符合闭包的定义。那么为什么它们叫“嵌套函数”,而不是“闭包”呢?

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

更新:我在阅读关于闭包的内容时,想到这个概念在Python中的应用。我搜索了一下,找到了下面评论中提到的文章,但我对那篇文章的解释完全理解不了,所以我才问这个问题。

10 个回答

79

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 scope 来存储变量。

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

    return counter

由于 scope 实际上只是一个引用,对它的字段进行的操作并不会真正修改 scope 本身,因此不会出现错误。

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
114

这个问题已经被 aaronasterling 回答过了

不过,可能有人会对变量是如何存储的感兴趣。

在进入代码片段之前:

闭包是从其外部环境中继承变量的函数。当你把一个函数作为回调传递给另一个进行输入输出操作的函数时,这个回调函数会在稍后被调用,而这个函数几乎是“魔法般”地记住了它被声明时的上下文,以及在那个上下文中可用的所有变量。

  • 如果一个函数不使用自由变量,它就不会形成闭包。

  • 如果有另一个内部层级使用自由变量——所有之前的层级都会保存词法环境(最后有示例)。

  • python < 3.X中,函数属性 func_closure 或在python > 3.X__closure__ 保存自由变量。

  • 每个 Python 函数都有闭包属性,但如果没有自由变量,它就是空的。

示例:闭包属性但没有内容,因为没有自由变量。

>>> def foo():
...     def fii():
...         pass
...     return fii
...
>>> f = foo()
>>> f.func_closure
>>> 'func_closure' in dir(f)
True
>>>

注意:自由变量 是创建闭包的必要条件。

我将使用上面的代码片段来解释:

>>> def make_printer(msg):
...     def printer():
...         print msg
...     return printer
...
>>> printer = make_printer('Foo!')
>>> printer()  #Output: Foo!

所有 Python 函数都有一个闭包属性,所以让我们检查与闭包函数相关的外部变量。

这里是函数 printer 的属性 func_closure

>>> 'func_closure' in dir(printer)
True
>>> printer.func_closure
(<cell at 0x108154c90: str object at 0x108151de0>,)
>>>

closure 属性返回一个包含定义在外部作用域中的变量详细信息的元组。

func_closure 的第一个元素可能是 None,也可能是一个包含函数自由变量绑定的单元元组,并且它是只读的。

>>> dir(printer.func_closure[0])
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__',
 '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
 '__setattr__',  '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>>

在上面的输出中,你可以看到 cell_contents,让我们看看它存储了什么:

>>> printer.func_closure[0].cell_contents
'Foo!'    
>>> type(printer.func_closure[0].cell_contents)
<type 'str'>
>>>

所以,当我们调用函数 printer() 时,它访问了存储在 cell_contents 中的值。这就是我们得到输出 'Foo!' 的原因。

我将再次使用上面的代码片段,做一些修改:

 >>> def make_printer(msg):
 ...     def printer():
 ...         pass
 ...     return printer
 ...
 >>> printer = make_printer('Foo!')
 >>> printer.func_closure
 >>>

在上面的代码片段中,我没有在 printer 函数内部打印 msg,所以它没有创建任何自由变量。由于没有自由变量,闭包内部也不会有内容。这正是我们在上面看到的。

现在我将解释另一个不同的代码片段,以清楚地说明 自由变量闭包 的关系:

>>> def outer(x):
...     def intermediate(y):
...         free = 'free'
...         def inner(z):
...             return '%s %s %s %s' %  (x, y, free, z)
...         return inner
...     return intermediate
...
>>> outer('I')('am')('variable')
'I am free variable'
>>>
>>> inter = outer('I')
>>> inter.func_closure
(<cell at 0x10c989130: str object at 0x10c831b98>,)
>>> inter.func_closure[0].cell_contents
'I'
>>> inn = inter('am')

所以,我们看到 func_closure 属性是一个闭包 单元 的元组,我们可以明确地引用它们及其内容——一个单元有属性 "cell_contents"

>>> inn.func_closure
(<cell at 0x10c9807c0: str object at 0x10c9b0990>, 
 <cell at 0x10c980f68: str object at   0x10c9eaf30>, 
 <cell at 0x10c989130: str object at 0x10c831b98>)
>>> for i in inn.func_closure:
...     print i.cell_contents
...
free
am 
I
>>>

在这里,当我们调用 inn 时,它会引用所有保存的自由变量,所以我们得到了 我是一自由变量

>>> inn('variable')
'I am free variable'
>>>
433

闭包是指一个函数可以访问它外部作用域中已经执行完的局部变量。

def make_printer(msg):
    def printer():
        print(msg)
    return printer

printer = make_printer('Foo!')
printer()

当调用 make_printer 时,会在栈上创建一个新的框架,这个框架里包含了 printer 函数的编译代码作为常量,以及 msg 的值作为局部变量。然后,它会创建并返回这个函数。因为 printer 函数引用了 msg 变量,所以在 make_printer 函数返回后,msg 仍然会被保留。

所以,如果你的嵌套函数没有:

  1. 访问外部作用域的局部变量,
  2. 在外部作用域执行时访问这些变量,

那么它们就不是闭包。

下面是一个不是闭包的嵌套函数的例子。

def make_printer(msg):
    def printer(msg=msg):
        print(msg)
    return printer

printer = make_printer("Foo!")
printer()  #Output: Foo!

在这里,我们把一个值绑定到参数的默认值上。这发生在创建 printer 函数的时候,因此在 make_printer 返回后,不需要保持对 printer 外部的 msg 值的引用。msg 在这个上下文中只是 printer 函数的一个普通局部变量。

撰写回答