以下内容基于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中的生成器函数是如何工作的?的更多相关文章

  1. [转载]python中multiprocessing.pool函数介绍

    原文地址:http://blog.sina.com.cn/s/blog_5fa432b40101kwpi.html 作者:龙峰 摘自:http://hi.baidu.com/xjtukanif/blo ...

  2. python中multiprocessing.pool函数介绍_正在拉磨_新浪博客

    python中multiprocessing.pool函数介绍_正在拉磨_新浪博客     python中multiprocessing.pool函数介绍    (2010-06-10 03:46:5 ...

  3. Python学习-39.Python中的生成器

    先回顾列表解释 lista = range(10) listb = [elem * elem for elem in lista] 那么listb就将会是0至9的二次方. 现在有这么一个需求,需要存储 ...

  4. 揭秘 Python 中的 enumerate() 函数

    原文:https://mp.weixin.qq.com/s/Jm7YiCA20RDSTrF4dHeykQ 如何以去写以及为什么你应该使用Python中的内置枚举函数来编写更干净更加Pythonic的循 ...

  5. python中的生成器(二)

    一. 剖析一下生成器对象 先看一个简单的例子,我们创建一个生成器函数,然后生成一个生成器对象 def gen(): print('start ..') for i in range(3): yield ...

  6. Python入门篇-生成器函数

    Python入门篇-生成器函数 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.生成器概述 1>.生成器generator 生成器指的是生成器对象,可以由生成器表达式得到, ...

  7. python中不需要函数重载的原因

    函数重载主要是为了解决两个问题: 1.可变参数类型 2.可变参数个数 并且函数重载一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函 ...

  8. python --- Python中的callable 函数

    python --- Python中的callable 函数 转自: http://archive.cnblogs.com/a/1798319/ Python中的callable 函数 callabl ...

  9. python中使用zip函数出现<zip object at 0x02A9E418>

    在Python中使用zip函数,出现<zip object at 0x02A9E418>错误的原因是,你是用的是python2点多的版本,python3.0对python做了改动 zip方 ...

随机推荐

  1. LED类代码

      /* led.c文件 标题: 点亮一个了LED灯 电路:开发板中P2口已接到LED灯的阴极 */ #include <reg52.h> #include "led1.h&qu ...

  2. 云栖大会上宣布即将开源的手淘Atlas什么来头?

    在刚刚过去的云栖大会上,手淘宣布其移动容器化框架Atlas将于2017年年初开源,对这个框架,在过去团队对外部做过一些分享,外界也一直对其十分关注,到现在它终于即将开源了. 本文将介绍Atlas的设计 ...

  3. Microsoft.mshtml.dll 添加引用及类型选择错误问题解决办法

    在比较早的文章中,提到使用 Microsoft.mshtml.dll 进行模拟浏览器点击的例子. 1.添加引用的问题 一般在开发环境下会在三个地方存有microsoft.mshtml.dll文件.所以 ...

  4. JVM Debugger Memory View for IntelliJ IDEA

    Posted on August 19, 2016 by Andrey Cheptsov Every day we try to find new ways to improve developer ...

  5. app v2界面

             相比v1要流畅了很多

  6. Knockout.Js官网学习(selectedOptions绑定、uniqueName 绑定)

    selectedOptions绑定 selectedOptions绑定用于控制multi-select列表已经被选择的元素,用在使用options绑定的<select>元素上. 当用户在m ...

  7. ThinkPhp 更改 BIT 类型的问题

    在使用ThinkPhp更改字段为BIT 类型的注意了,您将会遇到以下错误信息:  这是因为ThinkPhp在使用setField或Save方法时,将你的值更改成了字符串类型 而TINYINT 类型是不 ...

  8. 微软BI 之SSIS 系列 - 两种将 SQL Server 数据库数据输出成 XML 文件的方法

    开篇介绍 在 SSIS 中并没有直接提供从数据源到 XML 的转换输出,Destination 的输出对象有 Excel File, Flat File, Database 等,但是并没有直接提供 X ...

  9. windows和linux文件输 - ftp

    1. linux到linux的复制直接用scp命令 但是windows下就麻烦点, 安装winscp, 配置用户名和密码即可随意拖拽了. 下载地址: 需要linux电脑的用户名和密码即可 2. win ...

  10. 大数模板 poj3982

    1. 这个模板不是自己写的,转载的别人转载的,还没学完c++的我,想写也没有那能力. 这个模板我用在了POJ的一道题上,传送门--POJ3982 一般大数的题,都可用这个模板解决,仅仅须要改动主函数就 ...