python中yield的用法(生成器的讲解)
Posted 呆呆象呆呆
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python中yield的用法(生成器的讲解)相关的知识,希望对你有一定的参考价值。
2 理解说明yield与生成器
- 在
Python
中,使用yield
的函数被称为生成器函数
(generator function
)。 生成器
有两种方法:next()
和send()
,都可以调用生成器yield
VSreturn
return
作为结尾的普通函数直接返回所有结果,程序终止不再运行,并销毁局部变量;yield
会产生一个断点,暂停函数,挂起函数,保存当前状态。并且在yield
处返回某个值,返回之后程序就不再往下运行了。yield
与return
他们都在函数中使用,并履行着返回某种结果的职责。
- 有
yield
的函数则返回一个可迭代的生成器generator对象
,你可以使用for循环
或者调用next()方法
,send()方法
遍历这个产生的实例化生成器对象
来提取结果。
2 yield生成迭代器与next()、send()方法
生成器函数
返回一个生成器对象,针对生成器对象
,python
提供了两个主要方法,一个是next()
一个是send()
。
2.1 next()函数
第一次对生成器对象
调用next()
,相当于启动生成器
,会从生成器函数
的第一行代码开始执行,直到第一次执行完yield语句
后,跳出生成器函数
。
之后调用next()
,进入生成器函数
后,会从yield语句
的下一句语句开始执行,然后重新运行到yield语句
,执行后,跳出生成器函数
。后面再次调用next()
,依此类推。即执行一次next()
则调用一次生成器函数
,直至抛出不可迭代的错误。
def fun_yield():
print("starting fun yield")
while True:
res = yield 4
print("判断yield之后是否继续执行",res)
g = fun_yield() # 调用这个函数只是会得到一个生成器
print("函数结果是一个生成器:",g)
print("对此生成器还是进行调用:")
print("第一次调用")
print("生成器的返回值",next(g))
print("第二次调用")
print("生成器的返回值",next(g))
print("第三次调用")
print("生成器的返回值",next(g))
- 程序开始执行以后,因为
fun_yield
中有yield
关键字,所以函数并不会真的执行,而是先得到一个实例化的生成器对象
,结果1
可以看出不会真的运行。 - 直到调用
next()
,fun_yield
正式开始执行,先执行函数中的print("starting fun yield")
,然后进入while
循环,结果2
可以看出 - 程序遇到
yield
关键字,然后把yield
理解为return
,return
了一个4之后,程序停止,并没有执行后面的print("判断yield之后是否继续执行",res)
操作,此时next(g)
语句执行完成,所以输出的前两行,接下来准备运行第二次调用,结果3
可以看出 - 执行下面的
print("生成器的返回值",next(g))
,这个时候和上面那个差不多,不过不同的是,这个时候是从上一次yield
停止的断点开始执行,也就是要执行print("判断yield之后是否继续执行",res)
操作,这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return
出去了,并没有给赋值操作的左边传参数,也就相当于说res
里面是没有内容的),所以这个时候res
赋值是None
,所以接着下面的输出就是None
,结果4
可以看出 - 之后的程序会继续在
while
里执行,又一次碰到yield
,这个时候同样return
出4,结果5
可以看出
说明:
- 理解
yield
和return与generator
的关系和区别:- 带
yield
的函数是一个生成器函数
,直接使用不会真正的运行,而是返回一个实例化的生成器对象,每次通过generator
迭代中会从yield
处得到返回数据
- 带
python
提供生成器
的方法next()
,next()
就相当于“下一步”生成哪个数,这一次的next()
开始的地方是接着上一次的yield
停止的地方,所以调用next()
的时候,生成器并不会将函数从头开始执行,只是接着上一步停止的地方开始,然后遇到yield
后,返回表达式,此步就结束。
2.2 send()函数
send()
函数和next()
函数其实很相似,唯一的区别在于send()
函数可以传入值,而next()
函数不能传值,可以这样说:
next(f) = f.send(None)
第一次调用时不能使用send()
发送一个非None
的值,否则会出错的,因为没有yield
语句来接收这个值。
这里给出一个例子:
def fun_yield():
print("starting fun yield")
while True:
res = yield 4
print("判断yield之后是否继续执行",res)
g = fun_yield() # 调用这个函数只是会得到一个生成器
print("函数结果是一个生成器:",g)
print("对此生成器还是进行调用:")
print("第一次调用")
print(next(g))
print("生成器的返回值",g.send(1))
print("第二次调用")
print("生成器的返回值",g.send(2))
print("第三次调用")
print("生成器的返回值",g.send(3))
主要变化:
之前res
的值是None
,现在变成1,2,3,因为send
是发送参数给res
,因为上面讲到,return
的时候,并没有把4赋值给res
,下次执行的时候只好继续执行赋值操作,只好赋值为None
了。
如果用send
的话,出现的情况是,先接着上一次(yield 4
之后)执行,先把接受到的1,2,3等赋值给了res
,然后执行打印的作用,程序执行再次遇到yield
关键字,yield
会返回后面的值后,程序再次暂停,直到再次调用next()
方法或send()
方法。
注意:
生成器
在迭代的过程中可以改变当前迭代值,而修改普通迭代器
的当前迭代值往往会发生异常,影响程序的执行。(因为生成器
有send()
方法)
def myList(num): # 定义生成器
now = 0 # 当前迭代值,初始为0
while now < num:
val = (yield now) # 返回当前迭代值,并接受可能的send发送值;
now = now + 1 if val is None else val # val为None,迭代值自增1,否则重新设定当前迭代值为val
my_list = myList(5) # 得到一个生成器对象
print my_list.next() # 返回当前迭代值
print my_list.next()
my_list.send(3) # 重新设定当前的迭代值
print my_list.next()
3 常用的代码例子
3.1 例子1 输出斐波那契数列的前N个数(比较常用的使用方式)
调用fab(5)
不会执行fab函数
,而是返回一个generator对象
。在for
循环执行时,每次循环都会执行fab
函数内部的代码,执行到yield b
时,fab
函数就返回一个迭代值,下次迭代时,代码从yield b
的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到yield
。
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 使用 yield
a, b = b, a + b
n = n + 1
for n in fab(5):
print(n,end = " ")
f = fab(5)
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
3.2 例子2 使用types和inspect库判断是否为生成器或生成器函数(关于类型判断的说明)
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 使用 yield
a, b = b, a + b
n = n + 1
from inspect import isgeneratorfunction
print(isgeneratorfunction(fab))
print(isgeneratorfunction(fab(5)))
import types
print(isinstance(fab, types.GeneratorType) )
print(isinstance(fab(5), types.GeneratorType))
要注意区分fab
和fab(5)
fab
是一个generator function
,而fab(5)
是调用generator function
返回的一个generator
,好比类的定义和类的实例的区别,fab
是无法迭代的,而fab(5)
是可迭代的。
3.3 例子3 多个生成器不影响
每次调用含有yield
的函数行成一个新的generator
实例,各实例互不影响。
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 使用 yield
a, b = b, a + b
n = n + 1
from inspect import isgeneratorfunction
f1 = fab(3)
f2 = fab(5)
print("f1:",next(f1))
print("f2:",next(f2))
print("f1:",next(f1))
print("f2:",next(f2))
print("f1:",next(f1))
print("f2:",next(f2))
print("f2:",next(f2))
print("f2:",next(f2))
3.4 例子4 文件读取
如果直接对文件对象调用read()
方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过yield
,不再需要编写读文件的迭代类,就可以轻松实现文件读取:
def read_file(fpath):
BLOCK_SIZE = 1024
with open(fpath, 'rb') as f:
while True:
block = f.read(BLOCK_SIZE)
if block:
yield block
else:
return
4 yield 优势
Python 2
中的一个特性:内建函数 range
和 xrange
。其中,range
函数返回的是一个列表,而 xrange
返回的是一个迭代器。在 Python 3
中,range
相当于 Python 2
中的 xrange
;而 Python 2 中的 range
可以用 list(range())
来实现。
Python
做出如下的优化主要原因就是:如果用List
的话,会占用更大的空间。这个时候在python2
中range(1000)
就默认生成一个含有1000个数的list
了,所以很占内存。
for n in range(1000):
a=n
这个时候可以用yield
组合成生成器实现
def foo(num):
print("starting...")
while num<10:
num=num+1
yield num
for n in foo(0):
print(n,end=" ")
之所以要提供这样的解决方案,是因为在很多时候,只是需要逐个顺序访问容器内的元素。大多数时候,不需要一口气获取容器内所有的元素。比方说,顺序访问容器内的前 5 个元素,可以有两种做法:
- 获取容器内的所有元素,然后取出前 5 个;
- 从头开始,逐个迭代容器内的元素,迭代 5 个元素之后停止。
显而易见,如果容器内的元素数量非常多(比如有 10 ** 8
个),或者容器内的元素体积非常大,那么后一种方案能节省巨大的时间、空间开销。
现在假设,有一个函数,其产出(返回值)是一个列表。而若知道,调用者对该函数的返回值,只有逐个迭代这一种方式。那么,如果函数生产列表中的每一个元素都需要耗费非常多的时间,或者生成所有元素需要等待很长时间,则使用 yield
把函数变成一个生成器函数,每次只产生一个元素,就能节省很多开销了。
LAST 参考文献
python中yield的用法详解——最简单,最清晰的解释_mieleizhi0522的博客-CSDN博客_yield
Python 中的黑暗角落(一):理解 yield 关键字 | 始终
以上是关于python中yield的用法(生成器的讲解)的主要内容,如果未能解决你的问题,请参考以下文章