闭包初探

看过了很多有关闭包的文章,也看了很多书,还是对闭包一知半解。听说闭包是面向对象系统实现的基础,但是一直不明白这其中有什么关系。今天看《Fluent Python》终于有点理解了!

我更新了一下之前的《理解Python的UnboundLocalError》这篇博文,这篇讨论了Python的作用域以及原因。建议读本文之前先阅读一下这篇。

维基百科是这样解释的:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。

这里的“自由变量”是一个技术术语,指的是没有在本地作用域绑定的变量。用通俗的话说,就是一个函数内使用的变量并没有在函数内定义,而是在函数外定义的。这个函数和它之外的这个变量就构成了闭包。“闭包”的概念很难理解,我觉得是因为你乍一听上面这段话,就算明白了什么意思,也想不出有什么用。请看下面这个例子(来自《Flunt Python》):

有一个商品,它的价格每天都会变动。我们想要求出它的平均价格(其实这个例子用Python的生成器更加方便)。

averager函数和series构成了闭包。虽然make_averager函数执行一次之后退出了,但是函数内部的变量series并没有被销毁,多次调用avg发现series确实是之前的状态。

假如说不用闭包这个特性,使用面向对象来实现,代码要写成这样:

从中可以体会到,其实面向对象的中对象的概念,包含函数和属性。而函数里面使用的对象的属性,而不是函数内的局部变量,这就是有闭包的支持。属性和函数形成了闭包,属性就不会被销毁。闭包可以引用函数外的变量,但是这个变量又可以不是全局变量,从而对外隐藏了内部的一个状态,从而即达到了维持一个状态的目的,又做到了封装。我想,这就是为什么“闭包”是面向对象的基础吧。

2018年1月3日更新:我又有了一个想法,上面这个例子是“不用闭包这个特性”,为了体会一下闭包的“封装”功能,假设我们“没有闭包”这个特性,那么第一个例子的代码中,内部函数将不能访问外部函数的局部变量,因为没有了“闭包”这个通道。按照Python寻找变量的规则(大多数编程语言的规则)会到上一层函数->全局变量这个顺序查找。即,我们的代码将写成这样:

因为averager函数内用到了一个内部的状态变量,这个变量要独立于函数调用的生命周期,所以如果没有闭包,我们只要在每次使用averager函数之前创建一个变量series供它使用。这样即破坏了函数封装的作用,又失去了函数黑箱调用的方便性。

有关Python中闭包的实现,因为Python有“一等函数”的特性(函数是对象),“自由变量”保存在函数的__code__.co_freevars中,然后在avg.__closure__中保存了cell对象,对应每一个co_freevarscell对象中的cell_content中保存了真实的值。

Python的这种实现有一个需要特别注意的陷阱。这种绑定闭包的方式是“迟绑定的”,这意味着,闭包绑定的值只有在函数真正运行的时候才去查询。在讨论for循环作用域的时候,Python邮件列表的邮件提到这段代码:

这段代码的执行结果是10个9,而不是0-9.因为只有lambda函数执行的时候才去查询i的值,这个时候i已经变成了9.一个“不太优美”的解决方法是使用默认参数立即绑定 lst.append(lambda i=i: i)。提醒一下,造成这个陷阱的原因不是Python的Lambda函数,lambda并没有什么特殊的,真正的原因是Python闭包的迟绑定实现方式。

很多博客将“闭包”和“匿名函数”混为一谈,通过这篇文章,显然这不是等价的。只不过只有是嵌套在其他函数中的函数才可能需要处理不是全局作用域的外部变量,而这种函数通常是匿名函数罢了。



闭包初探”已经有4条评论

  1. Python的这种实现有一个需要特别注意的陷阱。这种绑定闭包的方式是“迟邦定的”, 这里存在 typo

  2. > 而函数里面使用的对象的属性,而不是函数内的局部变量,这就是有闭包的支持。

    的意思是不是:

    函数里面使用的「对象的属性」可以不是函数内的局部变量,这是因为有闭包的支持。

Leave a comment

您的电子邮箱地址不会被公开。 必填项已用 * 标注