Python 中闭包概念的条分缕析

简介: Python 中闭包概念的条分缕析

以前学 js 的时候第一次见到闭包,当时不甚了了,还为了应付面试强行记住了一个模棱两可的“定义”:在函数中嵌套定义函数,并且在外层将内层函数返回,一同返回了外层函数的环境。当时从字面意思以及当时一个经典例子试图去理解闭包,加之"闭包"这个翻译也很不容易让人看出其中的道理,导致对其总感觉懵懵懂懂。之前工作需要,用起 Python,又遇到闭包,并且看到了一些新奇有趣的资料,这才算大致把一些字面上的概念(first-class functions,bind,scope等等)贯通在一起,反过来对闭包有了更深的理解。

参考资料列在最后,十分推荐大家去读读。

概要

计算机中有些英文专业词汇,字面直译,难免因缺少上下文而显得苍白拗口,须得多方铺垫,方能味得古怪下面的原理。闭包(closure)便是一个这样牵扯了许多上下文的概念,包括编程语言最基本的绑定(binding),环境(environments),变量作用域(scope)以及函数是第一等公民(function as the first-class)等等。

Binding(绑定)

在Python中,binding(绑定) 是编程语言最基本的抽象手法,它将一个值绑定到一个变量上,并且稍后可以引用或者修改该变量。下面是几种不同层次的绑定,每组语句在运行时将一个名字与对应值绑定到其定义所在的环境中。

  • 将名字绑定到一块内存,通过赋值语句实现,当然函数调用时,形参和实参结合也是绑定:
In [1]: square = 4
  • 将名字绑定到一组复合运算,即函数定义,利用 def 关键字实现:
In [1]: def square(x):
            return x*x
  • 将名字绑定到一个数据集合,即类定义,使用 class 实现:
In [1]: class square:
            def __init__(self, x):
                self.x = x
            def value(self):
                return self.x * self.x

依照执行顺序,同名多次绑定,后面会覆盖前面

In [1]: square = 3
In [2]: square
Out[2]: 3
In [3]: def square(x):
   ...:     return x * x
   ...:
   ...:
In [4]: square
Out[4]: <function __main__.square(x)>
In [5]: class square:
   ...:     def __init__(self, x):
   ...:         self.x = x
   ...:
In [6]: square
Out[6]: __main__.square

说这些都是抽象,是因为它们提供了对数据、复合操作或数据集合的封装手段,即将一个名称与复杂的数据或逻辑进行捆绑,使调用者不用关心其实现细节,并可以据此来构建更复杂的工程。可以说绑定是编程的基石。

回到本文的主题上来,闭包是对一组复合语句的抽象,也就是函数,只不过是一种特殊的函数,至于这个特殊性在哪,这里先卖个关子,等稍后引入更多概念后再进行阐述。

Scope (作用域)

scope(作用域),顾名思义,也就是某个binding 能罩多大的范围,或者说可以在多大范围内访问的到一个变量。每个函数定义会生成一个局部定义域。

Python,和大多数编程语言一样,使用的是静态作用域(static scoping,有时也称 lexical scoping)规则。在函数嵌套定义的时候,内层函数内可以访问外层函数的变量值。因此你可以把作用域想象成一个容器,即它是可以嵌套的,并且内层作用域会扩展外层作用域,而最外层作用域即全局作用域。

上一小节提到了,多次同名绑定,后面会覆盖先前,其实有隐含前提:在同一作用域内。如果是嵌套作用域,其实是隐藏的关系,内层函数的变量定义会遮蔽外层函数同一名字定义,但是在外层作用域中,该变量仍是原值:

In [16]: a = 4
In [17]: def outer(): 
    ...:     a = 5
    ...:     print(a)
    ...:     def inner():
    ...:         a = 6
    ...:         print(a)
    ...:     inner()
    ...:     print(a)
    ...:
In [18]: outer()
5
6
5
In [19]: print(a)
4

作用域其实也可以从另一个角度理解,即我们在某个环境(environment)中,在确定一个name binding 值的时候,会从最内层作用域顺着往外找,找到的第一个该名字 binding 的对应的值即为该 name 引用到的值。

需要强调的时候,函数的嵌套定义会引起定义域的嵌套,或者说环境扩展(内层扩展外层)关系。类的定义又稍有不同,class 定义会引入新的 namespace(命名空间),命名空间和作用域是常拿来对比的概念,但这里按下不表,感兴趣的可以自己去查查资料。

说到这里,要提一下,一个常被说起的反直觉例子:

In [50]: a = 4
In [51]: def test():
    ...:     print(a) # 这里应该输出什么?
    ...:     a = 5
    ...:
In [52]: test()
---------------------------------------------------------------------------
UnboundLocalError                         
Traceback (most recent call last)
<ipython-input-52-fbd55f77ab7c> in <module>()
----> 1 test()
<ipython-input-51-200f78e91a1b> in test()
      1 def test():
----> 2     print(a)
      3     a = 5
      4
UnboundLocalError: local variable 'a' referenced before assignment

想象中,上面 print 处应该输出 4 或者 5 才对,为什么会报错呢?这是因为 test 函数在被解释器解析的时候,分词器会扫一遍 test 函数定义中的所有 token(符号),看到赋值语句 a=5 的存在,就会明确 a 是一个局部变量,因此不会输出 4。而在执行到 print(a) 的时候,在局部环境中,a 还未被binding,因此会报 UnboundLocalError

稍微扩展说明一下,虽然 Python 是解释执行的,即输入一句,解释一句,执行一句。但是对于代码块(即头部语句,冒号与其关联的缩进块所构成的复合语句(compound sentences),常见的有函数定义,类定义,循环语句等等)来说,还是会整体先扫一遍的。

First-Class Function(函数是第一等公民)

一般来说,组成编程语言的元素,如变量、函数和类,会被设定不同的限制,而具有最少限制的元素,被我们称为该编程语言中的一等公民。而一等公民最常见的特权有:

  1. 可以被绑定到名字上
  2. 可以作为参数在函数中传递
  3. 可以作为返回值被函数作为结果返回
  4. 可以被包含在其他数据结构中

套用到 Python 中的函数,即一个函数可以被赋值给某个变量,可以被其他函数接收和返回,可以定义在其他函数中(即嵌套定义):

In [32]: def test():
    ...:     print('hello world')
    ...:
In [33]: t = test # 赋值给变量
In [34]: t()
hello world
In [35]: def wrapper(func):
    ...:     print('wrapper')
    ...:     func()
    ...:
In [36]: wrapper(t) # 作为参数传递
wrapper
hello world
In [37]: def add_num(a): 
    ...:     def add(b): # 嵌套定义
    ...:         return a + b
    ...:     return add # 作为函数的返回值
    ...:
    ...:
In [38]: add5 = add_num(5)
In [39]: add5(4)
Out[39]: 9

并不是在所有语言中,函数都是一等公民,比如 Java8 以前的 Java,上面四项权利 Java7 中的函数后几项都没有。使用函数作为第一等公民的做法,我们成为函数式编程。在这个大数据时代,由于对并发的友好性,传统过程式语言(比如 Cpp、Java)都在新版本上逐渐支持函数式编程范式。

在这里,能够操作其他函数的函数(即以其他函数作为参数或者返回值的函数),叫做高阶函数。高阶函数使得语言的表达能力大大增强,但同时,也增加了编程复杂度。

Stack Call(栈式调用)

每个函数调用,会在环境中产生一个 frame栈帧),并且在栈帧中会进行一些绑定,然后压入函数调用栈中。在函数调用结束时,栈帧会被弹出,其中所进行的绑定也被解除,即垃圾回收,对应的局部作用域也随之消亡。

In [47]: def test():
    ...:     x = 4
    ...:     print(x)
    ...:
In [48]: test()
4
In [49]: x
---------------------------------------------------------------------------
NameError                                 
Traceback (most recent call last)
<ipython-input-49-6fcf9dfbd479> in <module>()
----> 1 x
NameError: name 'x' is not defined

即在调用结束后,局部定义的变量  x 在外边是访问不到的。但是如之前例子中,返回的 add 函数却引用了已经调用结束的 add_num 中的变量 a,怎么解释这种现象呢?可以记住一条,也是之前提到过的:

函数嵌套定义时,内部定义的函数所在的环境会自动扩展其定义所在环境

因此在外部函数返回后,返回的内部函数依然维持了其定义时的扩展环境,也可以理解为由于内部函数引用的存在,外部函数的环境中所有的绑定并没有被回收。

Closure(闭包)

千呼万唤始出来,以为是高潮,其实已结束。

闭包就是建立在前面的这些概念上的,上面提到的某个例子:

In [37]: def add_num(a): 
    ...:     def add(b): # 嵌套定义
    ...:         return a + b
    ...:     return add # 作为函数的返回值
    ...:
    ...:
In [38]: add5 = add_num(5)
In [39]: add5(4)
Out[39]: 9

其实就是闭包。捡起之前伏笔,给出我对闭包的一个理解:它是一种高阶函数,并且外层函数(例子中的add_num)将其内部定义的函数(add)作为返回值返回,同时由于返回的内层函数扩展了外层函数的环境,也就是对其产生了一个引用,那么在调用返回的内部函数(add5)的时候,能够引用到其(add)定义时的外部环境(在例子中,即 a 的值)。

结语

说了这么多,其实只是在逻辑层面或者说抽象层面去解释闭包是什么,常跟哪些概念纠缠在一起。但这些都没有真正触到其本质,或者说依然是空中楼阁,如果想要真正理解闭包,可以去详细了解下 Python 的解释执行机制,当然,那就是编译原理的范畴了。

参考

  1. cs61a 课程资料:composing programs,也是 SICP 一书的配套课程,书是神书,课程是好课程,资料更是有趣,不妨一读:http://composingprograms.com/
  2. 谷歌到的不错的文章:A Python Tutorial To Understanding Scopes and Closures:https://medium.com/@dannymcwaves/a-python-tutorial-to-understanding-scopes-and-closures-c6a3d3ba0937
相关文章
|
存储 数据挖掘 数据库
探索Python编程:从基础到高级探索移动应用开发之旅:从概念到实现
【8月更文挑战第29天】本文将带你进入Python的世界,无论你是初学者还是有一定经验的开发者。我们将从Python的基础知识开始,然后逐步深入到更复杂的主题。你将学习到如何编写清晰、高效的代码,以及如何使用Python进行数据分析和网络编程。最后,我们将介绍一些高级主题,如装饰器和生成器。让我们一起开始这段旅程吧!
|
11月前
|
存储 缓存 算法
Python闭包|你应该知道的常见用例(下)
Python闭包|你应该知道的常见用例(下)
Python闭包|你应该知道的常见用例(下)
|
11月前
|
自然语言处理 小程序 测试技术
Python闭包|你应该知道的常见用例(上)
Python闭包|你应该知道的常见用例(上)
Python闭包|你应该知道的常见用例(上)
|
10月前
|
Python
闭包(Closure)是**Python中的一种高级特性
闭包(Closure)是**Python中的一种高级特性
136 8
|
11月前
|
机器学习/深度学习 自然语言处理 语音技术
Python在深度学习领域的应用,重点讲解了神经网络的基础概念、基本结构、训练过程及优化技巧
本文介绍了Python在深度学习领域的应用,重点讲解了神经网络的基础概念、基本结构、训练过程及优化技巧,并通过TensorFlow和PyTorch等库展示了实现神经网络的具体示例,涵盖图像识别、语音识别等多个应用场景。
363 8
|
测试技术 Python
探索Python中的装饰器:从基础概念到高级应用
本文深入探讨了Python中一个强大而灵活的特性——装饰器。从其基本定义出发,逐步解析装饰器的本质、运作机制以及如何高效利用这一工具来优化代码结构、增加功能和提升代码的可读性与可维护性。通过具体示例,包括自定义简单装饰器、带参数装饰器、多重装饰等高级话题,本文展示了装饰器在软件开发中的广泛应用,旨在为读者提供一个全面而实用的装饰器使用指南。
|
12月前
|
Python
深入理解Python中的闭包
深入理解Python中的闭包
129 0
|
前端开发 JavaScript 数据可视化
Python+Dash快速web应用开发——基础概念篇
Python+Dash快速web应用开发——基础概念篇
399 3
|
Python
Python函数式编程:你真的懂了吗?理解核心概念,实践高阶技巧,这篇文章带你一次搞定!
【8月更文挑战第6天】本文介绍了Python中的函数式编程,探讨了高阶函数、纯函数、匿名函数、不可变数据结构及递归等核心概念。通过具体示例展示了如何利用`map()`和`filter()`等内置函数处理数据,解释了纯函数的一致性和可预测性特点,并演示了使用`lambda`创建简短函数的方法。此外,文章还强调了使用不可变数据结构的重要性,并通过递归函数实例说明了递归的基本原理。掌握这些技巧有助于编写更清晰、模块化的代码。
186 3

热门文章

最新文章

推荐镜像

更多