python中的生成器函数是如何工作的?

Posted 我爱你,中国!中国加油,武汉加油!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python中的生成器函数是如何工作的?相关的知识,希望对你有一定的参考价值。

以下内容基于python3.4

 

1. python中的普通函数是怎么运行的?

当一个python函数在执行时,它会在相应的python栈帧上运行,栈帧表示程序运行时函数调用栈中的某一帧。想要获得某个函数相关的栈帧,则必须在调用这个函数且这个函数尚未返回时获取,可能通过inspect模块的currentframe()函数获取当前栈帧。

栈帧对象中的3个常用的属性:

    • f_back : 调用栈的上一级栈帧
    • f_code: 栈帧对应的c
    • f_locals: 用在当前栈帧时的局部变量;

比如:

>>> import inspect
>>> def func():
...   global x
...   x = inspect.currentframe()
... 
>>> x = None
>>> func()
>>> x
<frame object at 0x7f50f3ee2868>

 

更进一步讲, 标准的python解释器是用C语言写的,通常称作CPython,  当执行一个python函数时,解释器中的C函数 PyEval_EvalFrameEx() 就会被调用,它来处理python 代码的字节码, 它的参数为对于python函数的栈帧 object,即上面例子中的 x就是一个栈帧对象。

举例说明函数是如何运行的?

>>> def foo():
...   x = 12
...   y = bar()
...   return y
... 
>>> def bar():
...   return hello
...

使用dis模块查看一下函数foo()的字节码(看不懂内容没事,其它有规律):

>>> import dis
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (12)
              3 STORE_FAST               0 (x)

  3           6 LOAD_GLOBAL              0 (bar)
              9 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             12 STORE_FAST               1 (y)

  4          15 LOAD_FAST                1 (y)
             18 RETURN_VALUE

运行过程:

解释器调用 C函数 PyEval_EvalFrameEx()运行foo()的字节码,它的参数为foo()对应的栈帧对象,运行位置为foo()对应的栈帧; 在运行过程中,遇到 CALL_FUNCTION 时,它会为函数bar()生成新的栈帧,然后又调用一个 PyEval_EvalFrameEx() 运行bar()对应的字节码,……,如此递归,然后一层层的返回;

 

2. 对于python中栈帧:

在python中的栈帧其实是在解释器的堆上分配内存的,所以,在一个python函数运行完成后,它的栈帧的仍然存在,并没有消失,下面例子说明了(当func函数运行完成后,我们然后可以访问到它对应的栈帧):

>>> import inspect
>>> def func():
...   global x
...   x = inspect.currentframe()
... 
>>> x = None
>>> func()
>>> x
<frame object at 0x7f50f3ee2868>
>>> x.f_code.co_name
func

 

3. python中的生成器函数是怎么运行的?

#这是一个函数
>>> def func():
...   print(You are SB)
... 
#这是一个生成器
>>> def gen():
...   yield You are SB
...   return ‘ni gei wo gun

对于函数与生成器函数的区别在于生成器中有yield表达式,  它们的co_flags是不相同的:

function没有*args或**kw时,func.__code__.co_flags=67;           function有*args没有**kw时,func.__code__.co_flags=71;

function没有*args有**kw时,func.__code__.co_flags=75;           function既有*args也有**kw时,func.__code__.co_flags=79;

function是一个generator时,func.__code__.co_flags=99.

>>> func.__code__.co_flags
67
>>> gen.__code__.co_flags
99

 

当运行一个生成器函数时,它会生成一个生成器:

>>> a = gen()
>>> type(a)
<class generator>
>>> b= gen()
>>> b
<generator object gen at 0x7f50f4a7a3f0>

上面例子中生成了两个生成器a与b, 每一个生成器都有两个常用的属性,分别为gi_frame与gi_code, 不同的生成器的gi_code是相同的,对应生成器函数的字节码,然而它们的gi_frame是不相同的,所以,不同的生成器可以分别运行,并且互不干扰;

 

对于每一个栈帧又都有一个指针f_lasti,它指向了最后执行的命令,在一开始没有执行时,它的值为-1;

>>> a.gi_frame.f_lasti
-1
>>> a.send(None)
You are SB
>>> a.gi_frame.f_lasti
3

>>> b.gi_frame.f_lasti
-1

 

当生成器执行到最后时,它就产生一个 StopIteration 异常,然后就停止了,当生成器函数中有return时, 这个异常的值就是return的值,如果没有return,异常的值为空;

>>> next(b)
You are SB
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: ni gei wo gun

 

 

生成器函数就就是这么运行的。

 

 

 

 

 

 

参考:A Web Crawler With asyncio Coroutines中的内容:

以上是关于python中的生成器函数是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

python中的函数生成器的工作原理

创建自己的代码片段(CodeSnippet)

Python代码阅读(第40篇):通过两个列表生成字典

编写高质量的Python代码系列之函数

如何优化C ++代码的以下片段 - 卷中的零交叉

Python如何生成器函数?