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

280

我曾经在Python中看到和使用过嵌套函数,它们符合闭包的定义。那么为什么它们被称为“嵌套函数”,而不是“闭包”呢?

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

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


8
有趣的是,通过谷歌搜索我找到了这篇文章,时间是2006年12月:http://effbot.org/zone/closure.htm。我不确定,在SO上是否不允许“外部副本”? - hbw
1
有关更多信息,请访问PEP 227--静态嵌套作用域 - Honest Abe
1
关于术语和定义的问题很少像人们想象的那样有帮助。 - Karl Knechtel
10个回答

432

闭包指的是一个函数可以访问其执行结束的封闭作用域中的局部变量。

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

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

调用make_printer时,会在堆栈上放置一个新的框架,其中包含编译后的printer函数的代码常量和msg的值作为本地变量。然后它创建并返回该函数。因为函数printer引用了msg变量,所以在make_printer函数返回后,它将继续存在。

所以,如果你的嵌套函数不会:

  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的普通局部变量。


2
你的回答比我的好得多,你说得很有道理。但是,如果我们要按照最严格的函数式编程定义,你的例子是否真正符合函数的定义呢?我已经很久没有接触了,我记不清楚严格的函数式编程是否允许不返回值的函数。如果你认为返回值为None,那么这个问题也就没有意义了,但这是另一个话题了。 - mikerobi
7
@mikerobi,我不确定我们是否需要考虑函数式编程,因为Python并不是真正的函数式语言,尽管它确实可以用作这样的语言。但是,内部函数并不是在函数式编程中的“函数”,因为它们的整个目的是创建副作用。不过,很容易创建一个函数来很好地说明这些点。 - aaronasterling
37
一个代码块是否是闭包取决于它是否关闭了环境,而不是你如何称呼它。它可以是例程、函数、过程、方法、块、子程序等等。在 Ruby 中,只有块可以作为闭包,方法不能。在 Java 中,方法不能作为闭包,但类可以。这并不会使它们变得没有闭包的特性。(虽然它们仅能关闭一些变量,并且无法修改它们,使它们几乎没用。) 你可以认为方法只是围绕着 self 封闭的过程。(在 JavaScript/Python 中几乎是正确的。) - Jörg W Mittag
5
请问"closes over"的定义是什么? "closes over"指的是一个函数捕获了自由变量的引用,以便可以在函数执行时使用它们。 - Evgeni Sergeev
6
“closes over”指的是从封闭作用域中引用一个局部变量[比如,i]。它可以查看(或更改)i的值,即使/当该范围“执行完毕”,即程序的执行已经前往代码的其他部分。定义i的块已经不存在了,但仍然可以引用i的函数。这通常被描述为“闭合变量i”。为了不处理特定的变量,可以实现为闭合整个环境帧,其中定义了该变量。 - Will Ness
显示剩余7条评论

114

问题已被aaronasterling回答。

然而,某些人可能对变量在底层是如何存储感兴趣。

在查看代码片段之前:

闭包是从其封闭环境继承变量的函数。当您将函数回调作为参数传递给将执行I/O的另一个函数时,此回调函数将稍后调用,并且这个函数将会——几乎像魔术一样——记住它声明的上下文以及该上下文中可用的所有变量。

  • 如果函数不使用自由变量,则不形成闭包。

  • 如果有另一个内部级别使用自由变量——除了词法环境保存以外的所有先前级别(示例在最后)

  • 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
>>>

NB: 自由变量是创建闭包必不可少的。

我将使用与上面相同的代码片段进行解释:

>>> 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或一个包含函数自由变量绑定的cell元组,且为只读。

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

在上面的代码片段中,我没有在打印机函数内打印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时,它将引用所有保存的自由变量,所以我们得到的是I am free variable
>>> inn('variable')
'I am free variable'
>>>

12
在Python 3中,func_closure现在被称为__closure__,类似于其他各种func_*属性。 - lvc
3
__closure__ 在 Python 2.6+ 中也可用,以保持与 Python 3 的兼容性。 - Pierre
“Closure” 指存储在函数对象中的闭包变量记录。它并不是函数本身。在 Python 中,__closure__ 对象就是闭包。 - Martijn Pieters
感谢@MartijnPieters的澄清。 - James Sapam
有人能解释一下为什么'inner'函数如何封闭'x'变量吗?据我所知,当调用'outer'时,'intermediate'函数被创建。但是在这个时候,'intermediate'函数不需要封闭'x'变量。因此,当调用'intermediate'函数时,'inner'函数被编译,但此时'inner'不应该访问'x'。 - Tushar Vazirani

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内创建一个对象作用域来存储变量。

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

28
有解决办法。在Python2中,你可以在外部作用域中创建 x = [0],然后在内部作用域中使用 x[0] += 1。在Python3中,你可以保持代码不变,并使用 nonlocal关键字 - unutbu
“虽然内部函数可以读取外部函数的变量,但它不能写入。” - 根据unutbu的评论,这是不准确的。问题在于当Python遇到类似x = ...的东西时,x被解释为局部变量,当然在那个时候还没有定义。另一方面,如果x是一个可变对象,并且具有可变方法,则可以很好地修改它,例如,如果x是支持inc()方法的对象,该方法会自行改变自身,x.inc()将可以正常工作。 - Thanh DK
4
还有一种选项,我认为严格来说比 #2 更好,即将 x 设为 counter 的属性(Property)。 - orome
10
Python 3有nonlocal关键字,类似于global关键字,但是作用于外部函数的变量。这使得内部函数可以重新绑定来自其外部函数的名称。我认为“绑定名称”比“修改变量”更准确。 - leewz
@Thanh DK 数组本质上与Object类相同。数组(或在这种情况下,Python列表)只是具有很多语法糖的类实例。因此,条目只是您修改的类变量,而类指针仍在工作。 - Sam96
显示剩余3条评论

23

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关键字将内部函数与显式提到的外部变量绑定起来,从而封闭它。 因此更明确地说它是一个'closure'。


3
有趣的是,参考网站 https://docs.python.org/3/reference/simple_stmts.html#the-nonlocal-statement。我不知道为什么在Python3文档中找到有关闭包的更多信息(以及从JS来的预期行为)不容易。 - user3773048
如果您创建两个闭包(closure)函数的实例会发生什么?您能否在使用部分添加第二个并行实例以补充您的答案? - Carlos Pinzón
@CarlosPinzón,没问题。我更新了答案以展示第二个实例。每个闭包都会创建自己的栈帧,因此关闭一个存在于一个实例中的变量不会受到另一个闭包的影响,除非它所关闭的变量是两个实例中的引用。希望这可以帮助你。 - Lee Benson

9

我曾经遇到需要一个独立但持久的命名空间的情况。

我使用了类。除此之外,我不用。

隔离但持久的命名可以用闭包实现。

>>> class f2:
...     def __init__(self):
...         self.a = 0
...     def __call__(self, arg):
...         self.a += arg
...         return(self.a)
...
>>> f=f2()
>>> f(2)
2
>>> f(2)
4
>>> f(4)
8
>>> f(8)
16

# **OR**
>>> f=f2() # **re-initialize**
>>> f(f(f(f(2)))) # **nested**
16

# handy in list comprehensions to accumulate values
>>> [f(i) for f in [f2()] for i in [2,2,4,8]][-1] 
16

6
def nested1(num1): 
    print "nested1 has",num1
    def nested2(num2):
        print "nested2 has",num2,"and it can reach to",num1
        return num1+num2    #num1 referenced for reading here
    return nested2

提供:

In [17]: my_func=nested1(8)
nested1 has 8

In [21]: my_func(5)
nested2 has 5 and it can reach to 8
Out[21]: 13

这是一个闭包的示例以及它如何被使用。


2

有些人对闭包的概念很困惑。闭包不是指内部函数,它的意思是关闭的行为。因此,内部函数是在非本地变量上进行封闭,这个变量被称为自由变量。

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()时,它会返回一个带有自由变量initial_valueinc函数。因此,我们创建了一个闭包。人们将inc称为闭包函数,我认为这让人们感到困惑,人们认为“内部函数是闭包”。实际上,inc不是闭包,因为它是闭包的一部分。为了简化生活,他们将其称为闭包函数。
  myClosingOverFunc=counter_in(2)

这会返回inc函数,该函数闭合了自由变量initial_value。当您调用myClosingOverFunc时。

 myClosingOverFunc() 

它会打印出2。

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

在我们的例子中,外部函数和内部函数中的initial_value将指向这个cell对象,而这个cell对象将指向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 创建了闭包。当 Python 看到这个时,它将创建一个 CELL 对象,cnt 将始终直接引用此 CELL 对象,CELL 将引用内存中存储 cnt 值的另一个对象。最初,cnt 的值为0。
 cnt   ======>>>>  CELL  =============>  0

当您调用内部函数并传递参数 counter(myFunc)() 时,这将增加 cnt 值 1。因此,我们的引用模式将如下更改:
 cnt   ======>>>>  CELL  =============>  1  #first counter(myFunc)()
 cnt   ======>>>>  CELL  =============>  2  #second counter(myFunc)()
 cnt   ======>>>>  CELL  =============>  3  #third counter(myFunc)()

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

counter(differentFunc)()

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

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


  

非常感谢你的回答,我学到了很多。我想指出一些从您的代码中不太清楚的地方,特别是使用 counter(fn) 的部分。你说 counter(myFunc)() 会将计数器增加1,非常好,但那之后呢?如果你像这样连续调用,计数器永远不会大于1。你忘记引入一个偏函数(我猜它代表自由变量并保持单元格存活),以便跟踪作用域:partial = counter(differentFunc); partial(); partial() - cards
...而counter(fn)的闭包是由两个自由变量fncnt参数化的。使用counter(myFunc).__code__.co_freevars和/或counter(myFunc).__closure__进行检查。 - cards
1
@cards 谢谢你的提醒。我有时间会去看一下。现在太忙了,连抓头发的时间都没有。 - Yilmaz

1
我可以帮助您进行翻译。以下是需要翻译的内容:

如果这有助于更清晰地说明问题,我想提供另一个关于Python和JS示例的简单比较。

JS:

function make () {
  var cl = 1;
  function gett () {
    console.log(cl);
  }
  function sett (val) {
    cl = val;
  }
  return [gett, sett]
}

执行中:
a = make(); g = a[0]; s = a[1];
s(2); g(); // 2
s(3); g(); // 3

Python:
def make (): 
  cl = 1
  def gett ():
    print(cl);
  def sett (val):
    cl = val
  return gett, sett

并执行:

g, s = make()
g() #1
s(2); g() #1
s(3); g() #1
原因:正如其他人所说,在Python中,如果在内部作用域中对具有相同名称的变量进行赋值,则会创建内部作用域中的新引用。但是在JS中不是这样的,除非您使用var关键字显式声明一个新的引用。

0

介绍了一种通过code对象来确定函数是否为闭包的方法。

正如其他答案中已经提到的那样,并非每个嵌套函数都是闭包。给定一个复合函数(代表整体操作)时,其中间状态可以是闭包或嵌套函数。 闭包是一种函数类型,其“参数化”其(非空)封闭范围即自由变量的空间。请注意,复合函数可以由两种类型组成。

(Python的)内部类型{{link1:code}}对象表示编译函数体。它的属性co_cellvarsco_freevars可用于“查看”函数的闭包/作用域。 如{{link2:doc}}中所述。

  • co_freevars: 自由变量的名称元组(通过函数闭包引用)
  • co_cellvars: 单元变量的名称元组(由包含作用域引用)。

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

让我们介绍一些支持函数

# 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_varnamesco_constsco_codeco_lnotabco_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

h1h2不是闭包,因为它们的作用域中没有cell和自由变量。 h3h4都是闭包,并且(在本例中)共享相同的cellc自由变量。 h4具有进一步的自由变量x,拥有自己的cell。


最终考虑:

  • __closure__ 属性和 __code__.co_freevars 可用于检查自由变量的值和名称(标识符)
  • nonlocal__code__.co_cellvars 之间的反类比(在广义上):nonlocal 作用于外部函数,而 __code__.co_cellvars 则作用于内部函数

0

针对《计算机程序的构造和解释》(SICP)读者:闭包这个词有两个毫无关联的意思(计算机科学和数学),详见维基百科了解后者/较少见的含义:

Sussman 和 Abelson 也在 1980 年代使用了闭包这个术语,但其第二个、无关的含义是指运算符的属性,即除了向数据结构中添加数据外,还能够添加嵌套的数据结构。这个术语的用法来自于数学方面的使用而非计算机科学先前的用法。作者认为这种术语重叠是“不幸的”。

第二个(数学)意义也在SICP in Python中使用,例如查看元组的讨论

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接