inkscape⽣成g代码_揭开asyncio的神秘⾯纱(4)-⽣成器原
理
前⾯⽂章有说道『协程和⽣成器最主要的特征:可以暂停执⾏和恢复执⾏』, 它另外⼀个隐含的意思:普通函数不能暂停和恢复执⾏。我们来看⼀个调⽤普通函数的例⼦:
def a():
print('hello, a')
def main():
print('before call a')
a()
print('after call a')
main()
# output:
eval是做什么的before call a
hello, a
after call a
可以发现,我们在 main 函数中调⽤ a 函数,main 函数会等 a 执⾏完毕直到它返回, 然后接着执⾏ main 函数后⾯的逻辑。
如果调⽤⽣成器函数:
def gen():
print('hello, before first yield')
yield
print('hello, before second yield')
yield
print('hello, gen finished')
def main():
print('before call a')
g = gen()
print('after call a')
print('start a')
g.send(None) # 启动⽣成器
print('resume a')
g.send(None)
main()
# output:
before call a
after call a
start a
hello, before first yield
resume a
hello, before second yield
可以发现,调⽤ gen() ⽣成器函数之后,main 函数并没有等待它执⾏完毕, 它是⽴即返回的,在后⾯,我们通过 g.send ⽅法启动⽣成器, 然后它会执⾏到 yield 的地⽅,然后暂停。我们在 main 函数再次调⽤ g.send ⽅法恢复⽣成器,gen ⼜会执⾏到下⼀个 yield 的地⽅。也就是说, 我们可以在 main 函数中控制 gen ⽣成器的暂停恢复。普通函数 a 则不能。
现象看到了,我们接着分析现象背后的本质:Python 是怎样实现函数调⽤的?
看到这个问题,如果读者计算机基础扎实的话,可能马上就能给出⼀个答案或者能想到⼀些线索。 ⽽对于没有学过计算机基础课程、或者上课在划⽔的同鞋,⼤家(我们) 之前可能根本没有想过这个问题,也不觉得这算是个问题。 就像住在地球上的我们,平时也不会去思考这个问题:地球是怎样实现⾃转的呢?
C 怎样实现函数调⽤?
在探索 Python 是如何实现函数调⽤之前,我们先看看 C 语⾔是怎样实现的。 假设我们有这样⼀段代码:
int p(){
return 1;
}
int main(int argc, char* argv[]){
int n = 1;
p();
return n;
}
运⾏时栈中。在这个例⼦中,
C 语⾔将每个函数运⾏时所需要的数据保存在运⾏时栈
当执⾏ main 函数的代码时,系统会创建⼀个栈帧,
把 main 函数所需要的数据(⽐如局部变量 n)圧⼊这个栈帧中
准备调⽤函数 p 之前,先把返回地址圧⼊栈中,接着创建⼀个新的栈帧⽤来执⾏函数 p
执⾏完 p 函数,程序会把 p 相关的数据从栈⾥⾯弹出
接着弹出返回地址,让程序继续从返回地址往后执⾏
光看⽂字有点抽象,这⾥有个图:
这个图较清晰的表⽰了:C 运⾏时栈的结构。我⾃⼰以前没有学习过操作系统这块相关的知识(或者也可能是我上课划⽔去了), 某⼀天我搜到这个的时候,看着还挺懵的:⽐如图中的 +4/%esp/%ebp 都是啥意思?我都不懂 =。=
不过感觉这些细节也不妨碍我理解这个函数调⽤过程,我是这样脑补的: C 语⾔有⼀个栈,当执⾏⼀个函数时,函数相关数据被圧进栈,当函数执⾏完,相关数据出栈。 ⽐如:函数 main 调⽤ a, 函数 a 调⽤ b。那这个栈⾥⾯就是 [main, a, b]。 当 b 执⾏完,栈⾥⾯就是[main, a]。当 a 和 main 都执⾏完,栈就空了,程序就退出了。
在这种情况下,有没有可能让函数 b 执⾏到⼀半,然后暂停呢?ummm, 好像不⾏啊,栈就是先进后出。
那如果我有另外⼀块内存中可以把 b 存起来,想执⾏的时候就把它压到栈中,暂停的时候把它从栈⾥拿出去。 这样是不是就可以实现:暂停、恢复这个函数了?ummm, 好像是可以,但我们先不管这个。
我们回到我们本来的问题:Python 是怎样实现函数调⽤的?
(ps: 突然发现这样表述有点问题,应该把问题改成 CPython 是怎样实现函数调⽤的,不过这样也不影响我理解这个原理。)CPython 是怎样实现函数调⽤的?
以前看到有⽂章说 Python 解释器可以看成⼀个基于栈的虚拟机(stack based virtual machine), 当时不懂为啥这么说,不懂的地⽅有两点:
1. 基于栈是啥意思?⾔外之意就是可以基于其它东西的喽?
2. 为啥说 Python 解释器是个虚拟机?我印象中的虚拟机是 VMWare/Virtual Box 这些东西。
但当我看完 C 语⾔函数调⽤栈之后,我似乎有点明⽩:
对于 C 语⾔代码,它通过编译器编译、链接等步骤⽣成指令,交给机器来执⾏。 代码执⾏的时候,机器会⽤在内存上开辟⼀块内存(栈)来保存⼀些运⾏时的信息。
⽽对于 Python 代码,它是由解释器来执⾏的,从这个⾓度看,CPython 解释器就是个虚拟的机器(虚拟机)。 ⽽在执⾏代码的时
候,CPython 可能也是⽤栈这种数据结构来实现函数调⽤等, 所以它就被叫作 stack-based。ummm,根据⾮常的科学。我觉得⾃⼰⾮常的机智。
后来读了更多的书、看了更多的资料,发现确实,上⾯这样理解基本是对的。在 CPython 中, 它有个结构体叫做 PyFrameObject,对应的还有个叫作 PyEval_EvalFrameEx 的函数,它是⽤来执⾏⼀个 frame。 PyFrameObject 基本是对标 C 栈帧,它⼤概长这个样⼦:
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
...
int f_lasti; /* Last instruction if called */
...
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;
f_back 指向前⼀个 frame
f_code 存了代码对象(脑补⼀下:多少和我们写的 Python 代码有点关系)
f_lasti 上⼀个执⾏过的字节码指令(Python 不是要先编译成字节码么,挺合理的)
f_localsplus[1] 是个啥,忽略
我们看⼀段简单的 Python 代码:
import inspect
def p():
n = 1
frame = inspect.currentframe()
print('p 函数所在栈帧t', id(frame))
print('栈帧上的局部变量t', list(frame.f_locals.keys()))
print('上⼀个栈帧t', id(frame.f_back))
def main():
print('main 函数栈帧t', id(inspect.currentframe()))
p()
if __name__ == '__main__':
main()
# output:
main 函数栈帧 4307710024
p 函数所在栈帧 4307665928
栈帧上的局部变量 ['n', 'frame']
上⼀个栈帧 4307710024
综合以上信息,这个 PyFrameObject 与 C 的栈帧确实很像:
1. 每个 frame 对象有个指针 f_back 指向之前那个函数的 frame,这样就形成⼀个栈的结构
2. frame 也保存了局部变量等信息。
3. 当函数执⾏完,frame 对象就再也不到了
给这个过程画个图(图来⾃⼀本⾮常好的书)
探索⽣成器函数的执⾏
看完了函数调⽤的例⼦,来看个⽣成器的例⼦:
import inspect
def gf():
frame = inspect.currentframe()
print('⽣成器所在的栈帧', id(frame))
print('上⼀个栈帧', id(frame.f_back))
yield
def main():
print('main 函数栈帧', id(inspect.currentframe()))
g = gf()
g.send(None)
if __name__ == '__main__':
main()
# output:
main 函数栈帧 4562411592
⽣成器所在的栈帧 4562367496
上⼀个栈帧 4562411592
看起来也没啥特殊的。但我们可以研究⼀下 g 这个⽣成器对象,可以发现 g 对象有个属性,叫做 gi_frame, 这是个啥,我很好奇 =.=。 于是我修改⼀下 main 函数代码,把这个对象的 id 打印出来。
def main():
print('main 函数栈帧', id(inspect.currentframe()))
g = gf()
g.send(None)
print(id(g.gi_frame))
# output:
main 函数栈帧 4459384904
⽣成器所在的栈帧 4459340808
上⼀个栈帧 4459384904
4459340808
发现它和⽣成器函数所在的栈帧是同⼀个! 这和⼀般函数⾮常的不⼀样, 普通函数执⾏完,它的栈帧也就没了,但⽣成器函数就不⼀发现它和⽣成器函数所在的栈帧是同⼀个!
有了函数的栈帧对象,也就是样:我们调⽤⽣成器函数, 它返回⼀个⽣成器对象给我们,并在这个对象上附上⽣成器函数的栈帧对象。 有了函数的栈帧对象,也就是说,我们可以随时控制这个函数的执⾏与暂停啦!
实际上,我们也确实可以通过 send 函数来恢复⽣成器的运⾏。我们可以看⼀眼 CPython 是怎样实现 send 函数的:ref
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论