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
生成器函数就就是这么运行的。
4.生成器相关操作:
1. X.__next__()方法和next()内置函数
当我们调用一个生成器函数时来生成一个生成器X时,这个生成器对象就会自带一个X.__next__()方法,它可以开始或继续函数并运行到下一个yield结果的返回或引发一个StopIteration异常(这个异常是在运行到了函数末尾或着遇到了return语句的时候引起)。也可以通过python的内置函数next()来调用X.__next__()方法,结果都是一样的;
>>> def gen():
... yield 'NI'
... return 'hahahaha'
... yield 'HAO'
...
>>> x = gen()
#查看一下x的属性,我们发现了__next__方法
>>> dir(x)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'send', 'throw'] #使用__next__方法运行函数;
>>> x.__next__()
'NI'
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: hahahaha #使用内置的next()函数运行函数(重新生成一个生成器x)
>>> x = gen()
>>> next(x)
'NI'
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: hahahaha
2. 生成器函数协议中的send()方法
在讲send()方法的时候,有必要了解一下next()或__next__()或send()语句执行时,生成器内的程序执行到了哪里暂停了。写一个很简单的函数,使用pdb调试一下:
#定义一个gen.py文件
1 def gen():
2 a = yield 1
3 b = yield 2
4 return 100
5
6 x = gen()
7 n1 = next(x)
8 n2 = next(x) #使用pdb调试一下这个文件
yinheyi@ubuntu:~/play$ python3.4 -m pdb gen.py
> /home/yinheyi/play/gen.py(1)<module>()
-> def gen():
在第7行设置一个断点
(Pdb) b 7
Breakpoint 1 at /home/yinheyi/play/gen.py:7
#运行到断点前
(Pdb) r
> /home/yinheyi/play/gen.py(7)<module>()
-> n1 = next(x)
#此时,可以使用 l 查看一下状态,显示运行第7行了;
(Pdb) l
2 a = yield 1
3 b = yield 2
4 return 100
5
6 x = gen()
7 B-> n1 = next(x)
8 n2 = next(x)
# 查看一下变量 n1的值,应该还没有定义,因为还没有运行到;
(Pdb) p n1
*** NameError: name 'n1' is not defined
# 查看一下生成器x的栈帧中的局部变量,应该是空,因为还没有开始执行生成器x
(Pdb)p x.gi_frame.f_locals
{}
# 执行第7行,使用next()开始执行了生成器x
(Pdb) n
> /home/yinheyi/play/gen.py(8)<module>()
-> n2 = next(x)
#再一次查看一个n1的值,它的值为1,即next( )的返回值,它的返回值就是第一个yield出来的值:1
(Pdb) p n1
1
# 再一次 查看一下生成器x的栈帧中的局部变量,竞然还为空,说明了什么??已经执行了yield 1的表达式,但是这个表达式执行到 yield出来1就暂停了,并没有执行到生成表达式“yiled 1” 的返回值 为None;所以,局部变量里面没有值;
(Pdb) p x.gi_frame.f_locals
{} #那就再执行第8行语句,看看会怎么样?
(Pdb) n
--Return--
> /home/yinheyi/play/gen.py(8)<module>()->None
-> n2 = next(x)
#打印 n2的值为2;
(Pdb) p n2
2
#查看一下生成器x的栈帧中的局部变量,这时,发现有了变量a, 没有变量b, 明白了,原来如此
(Pdb) p x.gi_frame.f_locals
{'a': None}
通过看上面的程序,我们知道,当next()或__next__()或send()语句执行时,在生成器里面的程序中它执行到 yiled value 这条语句, 它yield出来了一个value值,但是没有执行yiled value表达式 的返回值它就暂停了;
现在说说send()方法:从技术上讲,yield是一个表达式,它是有返回值的,当我们使用内置的next()函数或__next__方法时,默认yield表达式的返回值为 None,它使用send(value)方法时,它可以把一个值传递给生成器,使得yield表达式的返回值为send()方法传入的值; 当我们第一次执行send()方法时,我们必须传入None值,因为第一次执行时,还没有等待返回值的yield表达式(虽然 send()方法会执行下一条yield语句,但是上面已经说明了它在还没有来得及执行yiled value表达式 的返回值时它就暂停了)
定义一个gen.py文件,里面的内容为:
1 def gen():
2 a = yield 1
3 print('a的值为:', a)
4 b = yield 2
5 print('b的值为:', b)
6 return '我要结束了'
7
8
9 x = gen()
10 print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
11 n1 = x.send(None)
12 print('第一个yield表达式yield出来的值为:', n1)
13 print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
14 n2 = x.send('love love love')
15 print('第二个yield表达式yield出来的值为:', n2)
16 print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
17 try:
18 n3 = x.send('TMDTMD')
19 except StopIteration:
20 print('我已经运行到末尾了,没有yield语句供我继续运行了')
21 finally:
22 print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') #运行结果:
yinheyi@ubuntu:~/play$ python3.4 gen.py
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
第一个yield表达式yield出来的值为: 1
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
a的值为: love love love
第二个yield表达式yield出来的值为: 2
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
b的值为: TMDTMD
我已经运行到末尾了,没有yield语句供我继续运行了
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
3. 生成器函数中的return 语句:
当生成器运行到了return语句时,会抛出StopIteration的异常,异常的值就是return的值; 另外,即使return后面有yield语句,也不会被执行;
>>> def gen():
... yield 'NI'
... return 'hahahaha'
... yield 'HAO'
...
>>> x = gen()
>>> x.__next__()
'NI'
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: hahahaha
4. 另外,一个生成器对象也有close方法与throw方法,可以使用它们提前关闭一个生成器或抛出一个异常;使用close方法时,它本质上是在生成器内部产生了一个终止迭代的GeneratorExit的异常;
# 使用 close方法提前关闭异常;
>>> x = gen()
>>> x.close()
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration #使用throw方法抛出异常
>>> x = gen()
>>> x.throw(StopIteration)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in gen
5. 最后一个要讲的内容:yield from
这个是在python3.0以后新增加的内容,可以让生成器delegate另一个生成器;
1. 举一个例子看看它是怎么往外 yield数据的???
#生成器函数1
>>> def fun():
... yield 1
... yield 2
... return 'hello'
... yield 3 #生成器函数2
>>> def call_fun():
... yield 'a'
... result = yield from fun()
... print(result)
... yield 'b'
... yield 'c' #运行;
>>> caller = call_fun()
>>> caller.send(None)
'a'
>>> caller.send(None)
1
>>> caller.gi_frame.f_lasti #此时,查看一下caller的指针指向14
14
>>> caller.send(None)
2
>>> caller.gi_frame.f_lasti #此时caller的指针仍然是指向14,说明caller生成器遇到yield from时被阻塞了;
14
>>> caller.send(None)
hello #说明了 yield from 表达式的返回值为生成器fun()中return的返回值;
'b'
>>> caller.gi_frame.f_lasti
22
>>> caller.send(None)
'c'
>>> caller.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
这个例子我们明白了两点:1. 当我们调用主生成器caller时,遇到yield from 时,它就会停下来,运行子生成器的程序, yield出来的数据就是子生成器里的数据;2. yield from 表达式的返回值为子生成器的return的值;
2. 举个例子看看它是怎么通过 send()方法往里传递数据的?
>>> def fun():
... a = yield 1
... print('yield 1的值为', a)
... b = yield 2
... print('yield 2 的值为', b)
... return '子生成器完成,我要返回了'
...
>>> def call_fun():
... x1 = yield 'a'
... print('yield a 的值为', x1)
... result = yield from fun()
... print(result)
... x2 = yield 'b'
... print('yield b 的值为', x2) #一步步运行;
>>> caller = call_fun()
>>> caller.send(None)
'a'
>>> caller.send('xiaoming')
yield a 的值为 xiaoming
1
>>> caller.send('xiao')
yield 1的值为 xiao
2
>>> caller.send('ming')
yield 2 的值为 ming
子生成器完成,我要返回了
'b'
>>> caller.send('hahahha')
yield b 的值为 hahahha
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
通过这个例子,我们明白了1点:当主生成器遇到yield from以后,我们通过 send()方法传入值最终传给了子生成器;
3. 通过 yield from ,可以嵌套调用生成器,比如:
>>> def fun1():
... yield 1
... yield 2
...
>>> def fun2():
... yield from fun1()
...
>>> def fun3():
... yield from fun2()
...
>>> def fun4():
... yield 'hello'
... yield from fun3()
... yield 'world'
... #运行
>>> a = fun4()
>>> next(a)
'hello'
>>> next(a)
1
>>> next(a)
2
>>> next(a)
'world'
>>> next(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
部分内容参考:A Web Crawler With asyncio Coroutines中的内容;
想要也了解更多,请参考python手册:https://docs.python.org/3/index.html
python中的生成器函数是如何工作的?的更多相关文章
- [转载]python中multiprocessing.pool函数介绍
原文地址:http://blog.sina.com.cn/s/blog_5fa432b40101kwpi.html 作者:龙峰 摘自:http://hi.baidu.com/xjtukanif/blo ...
- python中multiprocessing.pool函数介绍_正在拉磨_新浪博客
python中multiprocessing.pool函数介绍_正在拉磨_新浪博客 python中multiprocessing.pool函数介绍 (2010-06-10 03:46:5 ...
- Python学习-39.Python中的生成器
先回顾列表解释 lista = range(10) listb = [elem * elem for elem in lista] 那么listb就将会是0至9的二次方. 现在有这么一个需求,需要存储 ...
- 揭秘 Python 中的 enumerate() 函数
原文:https://mp.weixin.qq.com/s/Jm7YiCA20RDSTrF4dHeykQ 如何以去写以及为什么你应该使用Python中的内置枚举函数来编写更干净更加Pythonic的循 ...
- python中的生成器(二)
一. 剖析一下生成器对象 先看一个简单的例子,我们创建一个生成器函数,然后生成一个生成器对象 def gen(): print('start ..') for i in range(3): yield ...
- Python入门篇-生成器函数
Python入门篇-生成器函数 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.生成器概述 1>.生成器generator 生成器指的是生成器对象,可以由生成器表达式得到, ...
- python中不需要函数重载的原因
函数重载主要是为了解决两个问题: 1.可变参数类型 2.可变参数个数 并且函数重载一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函 ...
- python --- Python中的callable 函数
python --- Python中的callable 函数 转自: http://archive.cnblogs.com/a/1798319/ Python中的callable 函数 callabl ...
- python中使用zip函数出现<zip object at 0x02A9E418>
在Python中使用zip函数,出现<zip object at 0x02A9E418>错误的原因是,你是用的是python2点多的版本,python3.0对python做了改动 zip方 ...
随机推荐
- 项目冲刺Forth
Forth Sprint 1.各个成员今日完成的任务 蔡振翼:修改部分博客 谢孟轩:续借功能和编辑资料功能的实现 林凯:初步实现登录功能 肖志豪:帮助其他人解决一些问题 吴文清:编写完善管理员个人界面 ...
- spring源码分析系列 (5) spring BeanFactoryPostProcessor拓展类PropertyPlaceholderConfigurer、PropertySourcesPlaceholderConfigurer解析
更多文章点击--spring源码分析系列 主要分析内容: 1.拓展类简述: 拓展类使用demo和自定义替换符号 2.继承图UML解析和源码分析 (源码基于spring 5.1.3.RELEASE分析) ...
- EF6 简单增删改查示例代码
示例一: private DbContext _dbContext; public DbContext CurrentContext { get { if (_dbContext == null) { ...
- python 环境搭建和Spyder的安装应用
http://blog.csdn.net/BurneAris/article/details/75214976
- http://blog.csdn.net/wzzvictory/article/details/16994913
原文地址:http://blog.csdn.net/wzzvictory/article/details/16994913 一.什么是instancetype instancetype是cla ...
- JS版日期格式化和解析工具类,毫秒级
/** * ===================================== * 日期相关方法 * ===================================== */ ;(fu ...
- request.getParameter(“xxx”)的参数的取值
request.getParameter(“xxx”)的参数的取值的几种可能: 1. Html中form表单中标签的name属性: <form name="form" met ...
- linux设置预留端口号,防止监听端口被占用 ip_local_reserved_ports
1. 背景 linux服务器启动时,会对指定的端口进行监听bind,如果同一个机器上这个端口已经被使用,则监听失败,程序无法启动. linux客户端连接服务器accept时,系统会分配本地临时端口用于 ...
- Android之官方导航栏ActionBar
一.ActionBar概述 ActionBar是android3.0以后新增的组件,主要用于标示应用程序以及用户所处的位置并提供相关操作以及全局的导航功能.下面我们就看看如何使用ActionBar,真 ...
- DHCP服务原理与搭建(Linux系统+路由器,二选一方案)
大家都知道上网的最基本前提是要在终端上设置IP.子网掩码.网关.DNS等地址信息,在家里或者在办公室很多时候打开电脑后发现就可以上网,并没有手动设置IP.掩码.DNS地址也能上网,这是什么原因呢?其实 ...