深入理解 python 虚拟机:原来虚拟机是这么实现闭包的

在本篇文章当中主要从虚拟机层面讨论函数闭包是如何实现的,当能够从设计者的层面去理解闭包就再也不用死记硬背一些闭包的概念了,因为如果你理解闭包的设计原理之后,这些都是非常自然的。

根据 wiki 的描述,a closure is a record storing a function together with an environment。所谓闭包就是将函数和环境存储在一起的记录。这里有三个重点一个是函数,一个是环境(简单说来就是程序当中变量),最后一个需要将两者组合在一起所形成的东西,才叫做闭包。

Python 中的闭包

我们现在用一种更加直观的方式描述一下闭包:闭包是指在函数内部定义的函数,它可以访问外部函数的局部变量,并且可以在外部函数执行完后继续使用这些变量。这是因为闭包在创建时会捕获其所在作用域的变量,然后保持对这些变量的引用。下面是一个详细的Python闭包示例:

def outer_function(x):
# 外部函数定义了一个局部变量 x def inner_function(y):
# 内部函数可以访问外部函数的局部变量 x
return x + y # 外部函数返回内部函数的引用,形成闭包
return inner_function # 创建两个闭包实例,分别使用不同的 x 值
closure1 = outer_function(10)
closure2 = outer_function(20) # 调用闭包,它们仍然可以访问其所在外部函数的 x 变量
result1 = closure1(5) # 计算 10 + 5,结果是 15
result2 = closure2(5) # 计算 20 + 5,结果是 25 print(result1)
print(result2)

在上面的示例中,outer_function 是外部函数,它接受一个参数 x,然后定义了一个内部函数 inner_function,它接受另一个参数 y,并返回 x + y 的结果。当我们调用 outer_function 时,它返回了一个对 inner_function 的引用,形成了一个闭包。这个闭包可以保持对 x 的引用,即使 outer_function 已经执行完毕。

在上面的例子当中 outer_function 的返回值就是闭包,这个闭包包含函数和环境,函数是 inner_function ,环境就是 x,从程序语义的层面来说返回值是一个闭包,但是如果直接从 Python 层面来看,返回值也是一个函数,现在我们打印两个闭包看一下结果:

>>> print(closure1)
<function outer_function.<locals>.inner_function at 0x102e17a60>
>>> print(closure2)
<function outer_function.<locals>.inner_function at 0x1168bc430>

从上面的输出结果可以看到两个闭包(从 Python 层面来说也是函数)所在的内存地址是不一样的,因此每次调用都会返回一个不同的函数(闭包),因此两个闭包相互不影响。

再来看下面的程序,他们的执行结果是什么?

def outer_function(x):
def inner_function(y):
nonlocal x
x += 1
return x + y return inner_function closure1 = outer_function(10)
closure2 = outer_function(20) result1 = closure1(5)
print(result1)
result1 = closure1(5)
print(result1)
result2 = closure2(5)
print(result2)

输出结果为:

16
17
26

根据上面的分析 closure1 和 closure2 分别是两个不同的闭包,两个闭包的 x 也是各自的 x ,因此前一个闭包的 x 变化并不会影响第二个闭包,所以 result2 的输出结果为 26。

闭包相关的字节码

在正式了解闭包相关的字节码之前我们首先来重新回顾一下 CodeObject 当中的字段:

def outer_function(x):
def inner_function(y):
nonlocal x
x += 1
return x + y print(inner_function.__code__.co_freevars) # ('x',)
print(inner_function.__code__.co_cellvars) # ()
return inner_function if __name__ == '__main__':
out = outer_function(1)
print(outer_function.__code__.co_freevars) # ()
print(outer_function.__code__.co_cellvars) # ('x', )

cellvars 表示在其他函数当中会使用本地定义的变量,freevars 表示本地会使用其他函数定义的变量。在上面的例子当中,outer_function 当中的变量 x 会被 inner_function 使用,而cellvars 表示在其他函数当中会使用本地定义的变量,所以 outer_function 的这个字段为 ('x', )。如果要了解详细的信息可以参考这篇文章 深入理解 python 虚拟机:字节码灵魂——Code obejct

上面的内容我们简要回顾了一下 CodeObject 当中的两个非常重要的字段,这两个字段在进行传递参数的时候非常重要,当我们在进行函数调用的时候,虚拟机会新建一个栈帧,在进行新建栈帧的过程当中,如果发现 co_cellvars 存储的字符串变量也是函数参数的时候,除了会在局部变量当中保存一份参数之外,还会将传递过来的参数保存到栈帧对象的其他位置当中(这里需要注意一下,CodeObject 当中的 co_freevars 保存的是字符串,也就是变量名,栈帧当中保存的是变量名字对应的真实对象,也就是函数参数),这么做的目的是为了方面后面字节码 LOAD_CLOSURE 的操作,因为实际虚拟机存储的是指向对象的指针,因此浪费不了多少空间。

实际在虚拟机的栈帧对象当中 freevars 是一个数组,后续的字节码都是会根据数组下标对这些变量进行操作。

下面我们分析一下和闭包相关的字节码操作

def outer_function(x):
def inner_function(y):
nonlocal x
x += 1
return x + y return inner_function if __name__ == '__main__':
import dis dis.dis(outer_function)

上面的代码回输出 outer_function 和 inner_function 对应的字节码:

  2           0 LOAD_CLOSURE             0 (x)
2 BUILD_TUPLE 1
4 LOAD_CONST 1 (<code object inner_function at 0x100757a80, file "closure_bytecode.py", line 2>)
6 LOAD_CONST 2 ('outer_function.<locals>.inner_function')
8 MAKE_FUNCTION 8 (closure)
10 STORE_FAST 1 (inner_function) 7 12 LOAD_FAST 1 (inner_function)
14 RETURN_VALUE Disassembly of <code object inner_function at 0x100757a80, file "closure_bytecode.py", line 2>:
4 0 LOAD_DEREF 0 (x)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_DEREF 0 (x) 5 8 LOAD_DEREF 0 (x)
10 LOAD_FAST 0 (y)
12 BINARY_ADD
14 RETURN_VALUE

我们现在来详细解释一下上面的字节码含义:

  • LOAD_CLOSURE:这个就是从栈帧对象当中加载指定下标的 cellvars 变量,在上面的字节码当中就是加载栈帧对象 cellvars 当中下标为 0 的对象,对应的参数就是 x 。也就是将参数 x 加载到栈帧上。
  • BUILD_TUPLE:从栈帧当中弹出 oparg (字节码参数) 个参数,并且将这些参数封装成元祖,在上面的程序当中 oparg = 1 。
  • LOAD_CONST:加载对应的常量到栈帧当中,这里是会加载两个常量,分别是函数对应的 CodeObject 和函数名。

在执行完上的字节码之后栈帧当中 valuestack 如下所示:

  • MAKE_FUNCTION:这条字节码的主要作用是根据上面三个栈里面的对象创建一个函数,其中最重要的字段就是 CodeObject 这里面保存了函数最重要的代码,最下面的元祖就是 inner_function 的 freevars,当虚拟机在创建函数的时候就已经把这个对象保存下来了,然后在创建栈帧的时候会将这个对象保存到栈帧。需要注意的是这里所保存的变量就是函数参数 x,他们是同一个对象。这就使得内部函数每次调用的时候都可以使用参数 x 。

我们再来看一下函数 inner_function 的字节码

  • LOAD_DEREF:这个字节码会从栈帧的 freevars 数组当中加载下标为 oparg 的对象,freevars 就是刚刚在创建函数的时候所保存的,也就是 outter_function 传递给 inner_function 的元祖。直观的来说就是将外部函数的 x 加载到 valuestack 当中。
  • STORE_DEREF:就是将栈顶的元素弹出,保存到 cellvars 数组对应的下标 (oparg) 当中。

后续的字节码就很简单了,这里不做详细分析了。

如果上面的过程太复杂,我们在这里从整体的角度再叙述一下,简单说来就是当有代码调用 outer_function 的时候,传递进来的参数,会在 outer_function 创建函数 inner_function 的时候当作闭包参数传递给 inner_function,这样 inner_function 就能够使用 outer_function 的参数了,因此这也不难理解,每次我们调用函数 outer_function 都会返回一个新的闭包(实际就是返回的新创建的函数),因为我们每次调用函数 outer_function 时,它都会创建一个新的函数,而这些被创建的函数唯一的区别就是他们的闭包参数不同。这也就解释了再之前的例子当中为什么两个闭包他们互不影响,因为函数 outer_function 创建了两个不同的函数。

总结

在本篇文章当中详细介绍了闭包的使用例子和使用原理,理解闭包最重要的一点就是函数和环境,也就是和函数绑定在一起的变量。当进行函数调用的时候函数就会创建一个新的内部函数,也就是闭包。在虚拟机内部实现闭包主要是通过函数参数传递和函数生成实现的,当执行 MAKE_FUNCTION 创建新函数的时候,会将外部函数的闭包变量 (在文章中就是 x ) 传递给内部函数,然后保存在内部函数当中,之后的每一次调用都是用这个变量,从而实现闭包的效果。


本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

深入理解 python 虚拟机:原来虚拟机是这么实现闭包的的更多相关文章

  1. 深入理解 Python 虚拟机:元组(tuple)的实现原理及源码剖析

    深入理解 Python 虚拟机:元组(tuple)的实现原理及源码剖析 在本篇文章当中主要给大家介绍 cpython 虚拟机当中针对列表的实现,在 Python 中,tuple 是一种非常常用的数据类 ...

  2. 深入理解 Python 虚拟机:浮点数(float)的实现原理及源码剖析

    深入理解 Python 虚拟机:浮点数(float)的实现原理及源码剖析 在本篇文章当中主要分析在 cpython 虚拟机当中 float 类型的实现原理以及与他相关的一些源代码. Float 数据结 ...

  3. 深入理解 Python 虚拟机:整型(int)的实现原理及源码剖析

    深入理解 Python 虚拟机:整型(int)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 内部是如何实现整型数据 int 的,主要是分析 int 类型的表示方式,分析 int ...

  4. 深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析

    深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 虚拟机当中是如何实现 复数 complex 这个数据类型的,这个数据类型在 ...

  5. 深入理解 Python 虚拟机:集合(set)的实现原理及源码剖析

    深入理解 Python 虚拟机:集合(set)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 虚拟机当中的集合 set 的实现原理(哈希表)以及对应的源代码分析. 数据结构介绍 ...

  6. 深入理解 Python 虚拟机:字典(dict)的实现原理及源码剖析

    深入理解 Python 虚拟机:字典(dict)的实现原理及源码剖析 在本篇文章当中主要给大家深入介绍一下在 cpython 当中字典的实现原理,在本篇文章当中主要介绍在早期 python3 当中的版 ...

  7. 深入理解 Python 虚拟机:字节(bytes)的实现原理及源码剖析

    深入理解 Python 虚拟机:字节(bytes)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 内部,bytes 的实现原理.内存布局以及与 bytes 相关的一个比较重要的优 ...

  8. 深入理解 Python 虚拟机:字典(dict)的优化

    深入理解 Python 虚拟机:字典(dict)的优化 在前面的文章当中我们讨论的是 python3 当中早期的内嵌数据结构字典的实现,在本篇文章当中主要介绍在后续对于字典的内存优化. 字典优化 在前 ...

  9. 深入理解 python 虚拟机:pyc 文件结构

    深入理解 python 虚拟机:pyc 文件结构 在本篇文章当中主要给大家介绍一下 .py 文件在被编译之后对应的 pyc 文件结构,pyc 文件当中的一个核心内容就是 python 字节码. pyc ...

  10. 深入理解 python 虚拟机:字节码灵魂——Code obejct

    深入理解 python 虚拟机:字节码灵魂--Code obejct 在本篇文章当中主要给大家深入介绍在 cpython 当中非常重要的一个数据结构 code object! 在上一篇文章 深入理解 ...

随机推荐

  1. Rust 声明式宏中的 Metavariables 有哪些

    Metavariables 官方文档确实写得很好,但是缺少一些风味,容易催眠‍ 还是直接看例子更爽一些,通常我们可以从示例代码中之间看出官方文档要表达的意思,而且很多时候我们可以直接在示例代码的基础上 ...

  2. docker-compose多服务器部署ELK

    多服务器构建ELK es使用1主2从,logstash转发,kibana展现,本文ELK版本使用7.16.1 如果单机测试,请查看docker-compose单服务器部署ELK es-master:1 ...

  3. PHP文件包含总结

    1.文件包含小知识 1.1 包含函数 PHP共有4个与文件包含相关的函数: include require include_once require_once include与include_once ...

  4. 消灭非稳态噪音的利器 - AI 降噪

    摘要:轻量级神经网络降噪方法,解析 ZegoAIDenoise 的算法实现! 文|即构引擎开发团队 一.轻量级神经网络降噪--ZegoAIDenoise 当下,用户在进行音频通话时常常置身于各种不同的 ...

  5. 2023年ccpc大学生程序设计竞赛-crf

    第一次参加这种大型线下程序设计比赛,心情自然是很激动,但比赛中并没有想象中那么顺利,甚至可以说有些惊险,比赛开始的时候,我们三人随便看看题,顺便等着跟榜,对于签到题我们自然是相信clk可以很快地独立完 ...

  6. 你不知道的 HTTP Referer

    前言 上周突然发现自己的自己站点的图片全都403了,之前还是好好的,图片咋就全都访问不了呢?由于我每次发文章都是先发了掘金,然后再从掘金拷贝到我自己的站点,这样我就不用在自己的站点去上传图片了,非常方 ...

  7. 2022-02-08 IValueConverter和StringFormat

    主页 后台 stringFormat

  8. pandas取出包含某个值的所有行

    pandas取出包含某个值的所有行df = df[df["from_account"].str.contains("fcwhx")] pandas取出不包含某个 ...

  9. mac一键获取最新datagrid 2017.3注册码到剪贴板

    mac一键获取最新datagrid 2017.3注册码到剪贴板 近期datagrid 校验激活码合法性的频率提高,导致需要频繁输入激活码 遂整理一脚本,自动获取最新注册码,并拷贝到剪贴板. 打开dat ...

  10. 《UNIX 传奇:历史与回忆》读后感

    <UNIX 传奇:历史与回忆> 是 bwk(Brian W. Kernighan)2019 年的新作,回忆了 UNIX 在大半个世纪的风雨历程,是一本引人入胜的书籍.通过对 UNIX 操作 ...