生成器和迭代器是python中非常重要也非常常用的两种类型,为了自己更好掌握和熟练的运用生成器最好的方式是从根本上了解生成器的原理,本文基于自身对python的理解,聊一下生成器的原理。 本文内容较为抽象,理解的不是恨透侧在实际使用中其实也没有多大影响,本人自身能力有限,在此能让大家对python生成器的原理有一个大概的印象即可。
python的原理
在开始生成器之前,想要先聊一下python函数的工作原理。我们都知道,在执行python函数前,python解释器会将.py
文件编译成字节码。python在执行函数前,pyhton解释器会使用一个叫做PyEvalFramEx的函数(C语言写的函数)去执行我们写的py函数。这很容易理解,因为python的解释器就是C编写的,因此可以说python是运行在C之上的。
我们随意写一个函数来对下面说明举例:
1
2
3
4
5
def func1():
func2()
def func2():
return True
- 首先PyEval_EvalFramEx在执行py函数时,会创建一个栈帧。这个栈帧其实是一个上下文。
- 在python中有一个理念:一切皆对象,即这个栈帧为对象,python解释器编译成的字节码也是对象。
我们可以查看一下编译后的字节码对象。python中可以使用dis来查看字节码。
1 2 import dis print(dis.dis(func1))执行完后我们可以看到输出的字节码如下
1 2 3 4 5 6 6 0 LOAD_GLOBAL 0 (func2) 2 CALL_FUNCTION 0 4 POP_TOP 6 LOAD_CONST 0 (None) 8 RETURN_VALUE None从字节码看出,每个字节码的左侧对应的是字节码的行号。对字节码的大致说明一下,首先是LOAD_GLOBAL加载了一个func2函数,然后CALL_FUNCTION说明调用了一个函数,POP_TOP从栈的顶端打印出来,接下来LOAD_CONST,func1没有返回于是将None加入进来,最后返回。 总结一下就是,在执行时创建一个栈帧上下文,然后在上下文中运行py函数。这个函数是全局唯一的(GLOBAL),当func1调用子函数func2,再次创建一个栈帧运行(CONST)。
所有栈帧都是分配在堆内存上,堆内存的特点是你要你不去释放,就会一直存在内存当中。这就决定了栈帧可以独立于调用者存在。即函>数退出后依然可以拿到函数的栈帧,这点和静态语言不一样,静态语言函数是放在栈上的,当函数执行完成退出后,栈就销毁了。 我们对上面的函数打印一下栈帧来证明一下。我们需要引入一个
inspect
包。
1 2 3 4 5 6 7 8 9 10 11 12 13 import inspect frame = None # 定义一个全局变量来接收func2函数的栈帧 def func1(): func2() def func2(): global frame frame = inspect.currentframe() # 获取func2的栈帧对象 func1() # 调用func1函数 print(frame.f_code.co_name) # 调用后func1和func2函数都退出,上面我们打印了func2函数的栈帧,当然退出之后我们也还是可以拿到func1函数的栈帧。 call_frame = frame.f_back # 拿到调用栈帧 print(call_frame.f_code.co_name)执行上面的代码,我们可以看到打印输出结果为:
func2
,func1
上面证明了即使函数执行完成,我们依然可以拿到函数当时运行的栈帧。引用官方的一张图如下:也很好的解释了这个过程,图中的foo’s bytrcode, bar’s bytecode等价于上面举例中的func1和func2函数的字节码。首先CPython的解释器使用PyEval_EvalFramEx函数创建一个PyFrameObject即栈帧的上下文对象,他的f_code指向foo函数的字节码,然后又创建一个PyFrameObject即栈帧的上下文对象,他的f_code指向bar函数的字节码,f_back指向创建他的栈帧对象。
上述大致为python函数的工作原理,大家对这个原理有一个概念即可,当然理解了也能帮助我们更容易的弄懂生成器的原理。聊这么多只是为了引出生成器的原理。
生成器的原理
上面介绍了python函数工作的原理,并解释了python所有的栈帧都是是分配在堆内存上。生成器正是基于这点才有了实现的可能。 我们简单写一个生成器代码
1
2
3
4
5
6
def gen_func():
yield 1
exec_1 = "fiest"
yield 2
exec_2 = "second"
return "end" # 对return做一下说明,python2版本生成器不能有return,python3的版本支持生成器带有返回值。
我们知道,当函数带有yield,那么这个函数就不是一个普通的函数,而是一个生成器函数。python解释器在编译的时候识别到该函数会将该函数识别为一个生成器对象。引用官方的一张生成器的原理图,如下:
在python的函数工作原理中我们已经介绍了PyFrameObject和PyCodeObject。生成器函数对象PyGenObject有个属性,分别是gi_frame和gi_code,分别指向PyFrameObject和PyCodeObject。很容易理解其实生成器对象其实就是对PyFrameObject和PyCodeObject进行了一次封装。在PyFrameObject中f_lasti属性指向的是最进一次执行的代码的字节码位置。 我们可以通过调用f_lasti和f_locals打印输出来验证一下是否如我们所说
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import dis
gen = gen_func()
print(dis.dis(gen)) # 打印一下gen_func编译后的机器码
print(gen.gi_frame.f_lasti) # 打印一下上一次执行位置
print(gen.gi_frame.f_locals) # 打印一下局部变量
print("++++++++++++++++++++++")
next(gen) # 调用一次gen
print(gen.gi_frame.f_lasti) # 打印一下上一次执行位置
print(gen.gi_frame.f_locals) # 打印一下局部变量
print("++++++++++++++++++++++")
next(gen) # 再次调用一次gen
print(gen.gi_frame.f_lasti) # 打印一下上一次执行位置
print(gen.gi_frame.f_locals) # 打印一下局部变量
执行后得到以下打印输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
5 0 LOAD_CONST 1 (1)
2 YIELD_VALUE
4 POP_TOP
6 6 LOAD_CONST 2 ('fiest')
8 STORE_FAST 0 (exec_1)
7 10 LOAD_CONST 3 (2)
12 YIELD_VALUE
14 POP_TOP
8 16 LOAD_CONST 4 ('second')
18 STORE_FAST 1 (exec_2)
9 20 LOAD_CONST 5 ('end')
22 RETURN_VALUE
None
-1
{}
++++++++++++++++++++++
2
{}
++++++++++++++++++++++
12
{'exec_1': 'fiest'}
第一次打印f_lasti得到结果-1表示没有执行。当我们使用next(gen)调用一次生成器,生成器停止在第一个yield处,由字节码我们可以看出第一个YIELD_VALUE在第二行,此时f_lasti=2。紧接着我们又进行一次next(gen)调用,生成器停止在第二个yield处,此时f_lasti=12,含有局部变量exec_1,打印输出f_locals为{‘exec_1’: ‘fiest’}。
以上就是对生成器原理的说明。希望能对大家对生成器原理有一个大概的认识。