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


249

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

嵌套函数不是因为没有被外部世界使用而不是闭包吗?

更新:我正在阅读有关闭包的知识,这让我开始思考关于Python的这个概念。我搜索并找到了某人在下面的评论中提到的文章,但是我无法完全理解该文章中的解释,所以这就是为什么我问这个问题。


8
有趣的是,一些谷歌搜索人员发现了我这个日期为2006年12月的信息:effbot.org/zone/closure.htm。我不确定-这样是否会使“外部重复项”不被接受?
hbw

Answers:


393

当函数可以从完成其执行的封闭范围访问局部变量时,就会发生关闭。

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创建函数时会发生这种情况,因此返回 后无需保留对msgexternal 值的引用。在这种情况下只是函数的普通局部变量。printermake_printermsgprinter


2
您的回答比我的要好得多,您的观点很好,但是如果我们要遵循最严格的函数式编程定义,那么您的示例甚至是函数?已经有一段时间了,我不记得严格的函数式编程是否允许不返回值的函数。如果您认为返回值是None,那是没有意义的,但这是另一个主题。
mikerobi

6
@mikerobi,我不确定我们是否需要考虑函数式编程,因为python并不是真正的函数式语言,尽管它当然可以使用。但是,不,内部函数不是那种意义上的函数,因为它们的全部目的是产生副作用。创建一个可以很好地说明要点的函数很容易,
aaronasterling

31
@mikerobi:一小段代码是否为闭包取决于其是否在其环境中关闭,而不是您所谓的闭包。它可以是例程,函数,过程,方法,块,子例程等。在Ruby中,方法不能是闭包,只能是块。在Java中,方法不能是闭包,但是类可以。但这并没有使它们变得封闭。(尽管它们仅对某些变量进行了封闭,而无法对其进行修改,这使它们几乎无用。)您可能会说方法只是对过程进行封闭的过程self。(在JavaScript / Python中几乎是正确的。)
JörgW Mittag

3
@JörgWMittag请定义“关闭”。
Evgeni Sergeev'5

4
@EvgeniSergeev“关闭”,即“指向i封闭范围内的局部变量[例如]”。指的是,即可以检查(或更改)其i值,即使该范围“已完成其执行”,即程序的执行已经移至代码的其他部分。i已定义的块不再存在,而引用的函数i仍然可以这样做。通常将其描述为“结束变量i”。为了不处理特定变量,可以将其实现为封闭定义该变量的整个环境框架。
内斯

103

这个问题已经由 aaronasterling 回答

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

在摘录之前:

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

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

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

  • 功能属性func_closure蟒<3.X或__closure__在python> 3.X节省的自由变量。

  • 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函数都有一个Closure属性,因此让我们检查与Closure函数关联的封闭变量。

这是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 VariableClosure

>>> 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,它将引用所有save free变量,因此我们得到I am free variable

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

9
在Python 3中,func_closure现在称为__closure__,与其他各种func_*属性类似。
lvc

3
__closure_可以在Python 2.6+中使用,以与Python 3兼容。–
Pierre

关闭是指存储附加到函数对象的封闭变量的记录。不是函数本身。在Python中,__closure__闭包是对象。
马丁·皮特斯

感谢@MartijnPieters的澄清。
詹姆斯·萨帕姆

71

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 scope内部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

27
有一些解决方法。在Python2中,您可以x = [0]在外部范围内使用,而x[0] += 1在内部范围内使用。在Python3中,您可以按原样保留代码,并使用nonlocal 关键字
unutbu 2014年

“虽然内部函数可以读取外部函数的变量,但不能写入它们。” -根据unutbu的评论,这是不正确的。问题在于,当Python遇到x = ...之类的东西时,x会被解释为局部变量,这当然在那时还没有定义。OTOH,如果x是使用可变方法的可变对象,则可以对其进行很好的修改,例如,如果x是支持inc()对其自身进行变异的方法的对象,则x.inc()将正常工作。
Thanh DK 2015年

@ThanhDK并不意味着您无法写入变量?当使用从可变对象调用方法时,您只是在告诉它对其进行修改,而实际上并不是在修改变量(该变量仅包含对该对象的引用)。换句话说,x即使您进行调用inc()或其他操作,变量所指向的引用也保持完全相同,并且您没有有效地写入变量。
user193130

4
还有另一种选择,使它x的属性counter严格优于#2 imv 。
orome

9
Python 3具有nonlocal关键字,类似于,global但用于外部函数的变量。这将允许内部函数从其外部函数重新绑定名称。我认为“绑定名称”比“修改变量”更准确。
leewz

16

Python 2没有闭包-它具有类似于闭包的解决方法。

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

在Python 3中,支持更为明确-简洁:

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

用法:

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

nonlocal关键词结合的内函数来明确提到的外变量,实际上包围它。因此,更明确地说是“关闭”。


1
有趣,供参考:docs.python.org/3/reference/…。我不知道为什么在python3文档中找到有关闭包的更多信息(以及来自JS的闭包的表现如何)并不容易吗?
user3773048 '18

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

这是什么是闭包以及如何使用闭包的示例。


0

我想在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

蟒蛇:

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关键字明确声明一个。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.