函数和生成器的执行流程、实现原理
首先浅显地介绍一下yield的实现原理,和题主的初衷有点偏离了,但即便如此相信对题主也是有帮助的。我们先来看看python是如何执行函数的
def mashiro():
...
def satori():
mashiro()
satori()
1.python虽然是解释型语言,但也要进行一次编译,编译成字节码对象。
2.python解释器去执行对应的字节码
3.当执行到satori函数的字节码时,会为其创建一个栈帧(Stack Frame),表示函数调用栈当中的某一帧,相当于一个上下文,函数要在对应的栈帧上运行。正所谓python中一切皆对象,栈帧也是一个对象(PyFrameObject),注意的是:"栈帧对象是存储在堆上面的,python中的对象本质上就是C语言中的malloc函数为结构体在堆上申请的一块内存"。这就意味着即便函数退出了,只要有指针指向它,就能拿到对应的栈帧,这一特性就决定了我们能够对函数进行非常精确的控制,也为后面的生成器实现埋下了伏笔。
4.然后会调用一个叫做PyEval_EvalFrameEx(PyFrameObject *f)的C语言函数,在satori函数对应的栈帧上去执行对应的字节码,参数就是satori函数的栈帧对象。
5.当执行到mashiro函数的字节码时同样会为其创建一个栈帧,然后把控制权交给新的栈帧对象,在mashiro函数对应的栈帧中运行mashiro函数的字节码
我们可以看看执行流程
我是一名python开发工程师,整理了一套python的学习资料,从基础的python脚本到web开发、爬虫、
数据分析、数据可视化、机器学习、面试真题等。想要的可以进群:688244617免费领取
import dis
def mashiro():
...
def satori():
mashiro()
dis.dis(satori)
"""
7 0 LOAD_GLOBAL 0 (mashiro)
2 CALL_FUNCTION 0
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
"""
# 首先LOAD_GLOBAL,把mashiro这个函数给load进来
# 然后CALL_FUNCTION,调用mashiro函数的字节码
# POP_POP,从栈的顶端把元素打印出来
# LOAD_CONST,我们这里没有return,所以会把None给load进来
# RETURN_VALUE,把None给返回
我们之
前说过可以获取栈帧,那怎么获取呢?
import inspect
frame = None
def mashiro():
global frame
name = "椎名真白"
home = "樱花庄"
frame = inspect.currentframe() # 拿到当前函数的栈帧
def satori():
name = "古明地觉"
home = "东方地灵殿"
mashiro()
# 但函数执行完,显然frame不会空
satori()
# 那么这个栈帧有啥属性呢?
# f_code:字节码,再调用co_name就能拿到函数名
print(frame.f_code.co_name) # mashiro
# f_back:调用者的上一级栈帧, 这里显然是satori函数的栈帧
print(frame.f_back.f_code.co_name) # satori
# f_locals:当前栈帧当中的局部变量
print(frame.f_locals) # {'name': '椎名真白', 'home': '樱花庄'}
print(frame.f_back.f_locals) # {'name': '古明地觉', 'home': '东方地灵殿'}
"""
因为我们拿到了栈帧,有一个全局变量在指向它,所以不会被销毁
那么与之关联的调用者的栈帧同样也不会被销毁,
因为当前栈帧的f_back指向了调用者的栈帧
"""
之前说过,栈帧是分配在堆内存上面的,也正因为如此,生成器才会实现。
是如何实现的呢?实际上是对PyFrameObject
做了一层封装(注意:如果函数内部有yield,那么在编译的时候就已经确定是生成器了),封装成了PyGenObject
,这个PyGenObject
有两个属性,一个是gi_frame:之前的PyFrameObject
,一个是gi_code:PyCodeObject(字节码)
,重点是这个gi_frame
,这里面除了之前的f_locals,还有一个最重要的f_lasti
def gen_func():
yield 123
name = "古明地觉"
yield 456
home = "东方地灵殿"
return "我永远喜欢古明地觉"
g = gen_func()
print(g.gi_frame.f_lasti) # -1
"""
结果是-1,说明在生成器刚创建的时候,f_lasti为-1
"""
# 我们send一下
try:
g.send("古明地觉")
except TypeError as e:
print(e) # can't send non-None value to a just-started generator
# 提示我们只能发送一个None
我们看一下源码
你看到了什么,是的,这个f_lasti就是标记生成器执行到哪一步了。
import dis
def gen_func():
yield 123
name = "古明地觉"
yield 456
home = "东方地灵殿"
yield 789
return "我永远喜欢古明地觉"
g = gen_func()
dis.dis(g)
"""
4 0 LOAD_CONST 1 (123)
2 YIELD_VALUE
4 POP_TOP
5 6 LOAD_CONST 2 ('古明地觉')
8 STORE_FAST 0 (name)
6 10 LOAD_CONST 3 (456)
12 YIELD_VALUE
14 POP_TOP
7 16 LOAD_CONST 4 ('东方地灵殿')
18 STORE_FAST 1 (home)
8 20 LOAD_CONST 5 ('我永远喜欢古明地觉')
"""
# 可以看到,有两个YIELD_VALUE,因为我们生成器当中有两个yield
# 最后的LOAD_CONST则是把'我永远喜欢古明地觉'load进来
# 然后返回
# f_lasti:标记生成器的执行状态
# f_locals:当前生成器里面的局部变量
print(g.gi_frame.f_lasti) # -1
print(g.gi_frame.f_locals) # {}
"""
我们创建了生成器,但是还没执行,因此f_lasti是-1,当前也没有局部变量
"""
g.__next__()
print(g.gi_frame.f_lasti) # 2
print(g.gi_frame.f_locals) # {}
"""
当我们__next__之后,f_lasti变成了2,
显然对应那个2 YIELD_VALUE,说明之前load 123,然后yield出去了
"""
g.__next__()
print(g.gi_frame.f_lasti) # 12
print(g.gi_frame.f_locals) # {'name': '古明地觉'}
"""
当走到第二个yield的时候,f_lasti是12
这个时候f_locals
注意:上面输出的
6 LOAD_CONST 2 ('古明地觉')
8 STORE_FAST 0 (name)
表示先把'古明地觉'这个字符串load进来,然后使用name变量进行存储
这个时候已经创建了相应的变量
"""
g.__next__()
print(g.gi_frame.f_lasti) # 22
print(g.gi_frame.f_locals) # {'name': '古明地觉', 'home': '东方地灵殿'}
"""
f_lasti为22,此时f_locals又多了一个元素,说明又创建了一个局部变量
"""
因此我们便很容易理解为什么生成器能够实现了,因为无论生成器执行到哪一步,内部PyGenObject的f_lasti都进行了完美的监督
,或者说知道并记录了生成器停下来的位置。我们可以通过yield使生成器暂停,并把值yield出来(可以把yield简单看成return,或者把生成器当成可以暂停的函数),并且还能通过send、next方法让其从停下来的地方开始前进,当然生成器的栈帧也是在堆上的,我们也是随时都可以拿到它,这说明我们不仅能够让其暂停、并从停下来的地方前进,还能随时都这样。也正因为可以随时控制它,python早期版本中协程才会得以实现,这也是协程能够实现的理论基础。
但是还有两个问题,为什么生成器只能生成一次,第二次执行就没了?
def gen():
yield 1
yield 2
yield 3
g = gen()
print(sum(g)) # 6
print(sum(g)) # 0
为什么使用生成器暂停之后可以使用send和next唤醒?
这两个问题可以合并一块回答
def gen():
yield 1
yield 2
yield 3
g = gen()
for _ in g:
print(g.gi_frame)
"""
<frame at 0x000002700....
<frame at 0x000002700....
<frame at 0x000002700....
"""
print(g.gi_frame) # None
我们看到当我们最后试图去拿栈帧的时候,居然返回的是None,这是因为f_lasti大限已至,走到了尽头,不可能再从头开始,除非你像函数一样,重新生成一个新的生成器。而且每一次执行到yield的时候会创建一个新的栈帧,然后将局部变量保存之后就把f_back清空,并将新的栈帧从栈帧链当中移除(注意:只是保存在了别的地方),然后当我们使用send、next的时候,又会将其插入到栈帧链当中执行。周而复始,直到f_lasti走到头。
yield和yield from
yield from又是个啥,当一个函数中出现了yield from,那么这个函数还有一个专业名词,叫做委托生成器
。目的就是在调用方和子生成器之间建立一个双向通道。
def gen():
yield "haha"
yield "gaga"
def foo():
yield from gen()
f = foo()
print(f.__next__()) # haha
"""
foo就是我们的委托生成器,gen就是我们的委托生成器
我们生成的f调用__next__是直接和gen通信的,不需要经过foo
"""
并且还有一个区别
def gen():
yield [1, 2, 3]
def foo():
yield from [1, 2, 3]
g = gen()
print(g.__next__()) # [1, 2, 3]
f = foo()
print(f.__next__()) # 1
"""
yield一次性将后面的值迭代出来
yield from后面是一个序列的话,那么只会迭代序列的第一个
"""
但是这有什么用呢?实际上,理解yield和yield from是理解后面的async和await的基础。yield from帮我们做了很多事情,比如捕获子生成器抛出的异常。我们如果想获取返回值的话,一般是通过捕获异常,但是有了yield from就方便很多了。
def gen():
yield 1
return "xxx"
def foo():
a = yield from gen()
print(a)
f = foo()
print(f.__next__())
try:
f.__next__()
except StopIteration:
import sys
print(sys.exc_info()[0])
"""
1
xxx
<class 'StopIteration'>
"""
"""
为什么会有上述这个结果
首先当我们进行send或者next的时候,就会走到下一个yield
当第二次next的时候,对于gen来说已经没有yield了,直接return了,按理说会报错的
这时候委托生成器就登场了
一旦子生成器return,yield from会拿到返回值,然后异常向上抛,会在委托生成器里面寻找yield,
如果委托生成器找不到yield,那么异常会继续抛出
"""
def gen():
yield 1
return "xxx"
def foo():
# 因此我们可以写成这种形式
# yield和yield from是可以赋值的,如a = yield, a = yield from
# a = yield,是当send的时候可以传入一个值然后赋给a
# a = yield from 子生成器调用 ,则是子生成器在返回的时候赋值给a
# 因此当gen()返回的时候,yield from gen()这个整体就相当于返回值"xxx"
# 然后我们再将这个返回值yield出来
yield (yield from gen())
f = foo()
f.__next__()
print(f.__next__()) # xxx
但是感觉好像也没啥用蛤。其实不然yield from做的事情远不止这些,当然之所以引入yield from,主要是为了引出async和await
async和await
这是python在3.5开始引入的两个关键字,专门用于创建协程,还提供了asyncio这个用于事件循环的异步网络库。很多人刚开始对这些新的概念不是很了解, 比如coroutine、future、task、event_loop等等。
我们来看一个例子:
import asyncio
async def foo1():
print("foo1")
await asyncio.sleep(2)
return 123
async def foo2():
await asyncio.sleep(1)
print("foo2")
async def bar():
res = await foo1()
print(res)
asyncio.run(asyncio.wait([bar(), foo2()]))
"""
foo1
foo2
123
"""
首先我们运行bar()和foo2()这两个协程,bar()里面await foo1(),这就相当于之前的yield from,在event_loop和foo1()之间建立一个双向通道,当打印完"foo1"的时候,阻塞了,因此要通知事件循环,注意:这一步是不需要经过bar()的,而是foo1()和事件循环直接通信。1s后,打印"foo2",最后foo1()执行结束,当返回的时候,await 会捕获到异常,并拿到返回值。所以await和yield from是有着异曲同工之妙的。
再比如tornado,早期在python还不支持原生协程的时候,tornado不得不使用生成器来模拟协程。
from tornado import web
from tornado import gen
class IndexHandler(web.RequestHandler):
@gen.coroutine
def get(self):
...
但是自从python提供了原生协程之后,tornado也支持使用async和await
from tornado import web
class IndexHandler(web.RequestHandler):
async def get(self):
...
现在可以这么定义,并且tornado之前的事件循环也改成了asyncio,不再使用自己之前的那一套了。怎么证明呢?
async def foo():
print("~~~foo~~~")
if __name__ == '__main__':
import tornado.ioloop
tornado.ioloop.IOLoop.instance().run_sync(foo)
"""
~~~foo~~~
"""
我们使用tornado也能启动,说明底层使用的都是同一个事件循环。当然其实官方也说了
以上就是对yield的一点简单介绍,觉得文章还不错的话不妨收藏起来慢慢看,有任何建议或看法欢迎大家在评论区分享讨论!