273

我在 Python 中看到并使用过嵌套函数,它们与闭包的定义相匹配。那么为什么他们被称为nested functions而不是closures

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

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

4

8 回答 8

424

当函数可以从已完成执行的封闭作用域访问局部变量时,就会发生闭包。

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!

在这里,我们将值绑定到参数的默认值。这发生在创建函数时,因此返回 后不需要维护对external toprinter值的引用。在这种情况下,它只是函数的普通局部变量。msgprintermake_printermsgprinter

于 2010-10-26T03:20:12.940 回答
111

aaronasterling已经回答了这个问题

但是,有人可能会对变量如何在后台存储感兴趣。

在进入片段之前:

闭包是从其封闭环境继承变量的函数。当您将函数回调作为参数传递给另一个将执行 I/O 的函数时,该回调函数将在稍后被调用,并且该函数将 - 几乎神奇地 - 记住声明它的上下文以及所有可用的变量在这种情况下。

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

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

  • python < 3.X 或func_closurepython > 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 函数都有一个闭包属性,所以让我们检查一下与闭包函数相关的封闭变量。

func_closure这是函数的属性printer

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

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

现在我将解释另一个不同的片段来清除所有Free Variable内容Closure

>>> 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'
>>>
于 2014-01-03T06:41:20.730 回答
80

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

并创建一个 Object scopeinsideinitCounter来存储变量

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
于 2014-05-09T07:22:27.090 回答
22

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内部函数绑定到显式提到的外部变量,实际上是封闭了它。因此更明确地是“关闭”。

于 2017-03-14T01:49:50.913 回答
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
于 2014-06-24T17:38:54.470 回答
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

这是一个关于闭包是什么以及如何使用它的示例。

于 2014-01-28T11:57:34.423 回答
3

人们对什么是闭包感到困惑。闭包不是内部功能。关闭的意思是关闭的行为。所以内部函数正在关闭一个称为自由变量的非局部变量。

def counter_in(initial_value=0):
    # initial_value is the free variable
    def inc(increment=1):
        nonlocal initial_value
        initial_value += increment
        return 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 Obj。它间接引用了 的值initial_value。这就是为什么即使外部函数的作用域消失了,内部函数仍然可以访问自由变量

假设我想编写一个函数,它将函数作为 arg 并返回该函数被调用的次数。

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始终直接引用此单元格 obj 并且 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 obj。我们刚刚创建了另一个闭包实例。

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


  
于 2020-09-30T08:32:28.307 回答
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关键字显式声明一个。

于 2017-06-30T11:45:46.530 回答