什么是生成器(Generator)
生成器:只有在被调用的时候才会产生相应的数据,它实现了内置的__next__()
方法,所以它是一种特殊的迭代器。
为什么要用生成器?:假如有一个list
,存放着1T甚至更大的数据量,但其中的每个数据你想需要的时候再取(通俗点讲就是仅仅想要遍历这个列表)。如果你直接把它加载至内存,内存就可能会不足。生成器就是为了解决此类内存占用,使用生成器能帮助你极大地降低内存并提高开发效率。
实现生成器的两种方式:
- 将平时用的列表生成式改为()便是一个生成器:如:
(i for i in range(100))
- 在函数内部使用
yield
关键词。(只要函数内部使用了yield,它便成了一个生成器)
生成器的特点
- 本身具有与迭代器相同的特性:可以通过
next()
单次取出或使用for
循环一次性取出。 - 取出数据的过程是不可逆的,且只能使用一次,生成器会被耗尽 。举例:
>>> gen = (i for i in range(10))
>>> type(gen)
<class 'generator'> # 这是一个生成器
>>> next(gen)
0
>>> next(gen)
1
>>> list(gen) # 这时是从2开始生成列表
[2, 3, 4, 5, 6, 7, 8, 9]
>>> list(gen) # 此时的生成器内部的指针已到末尾,返回空
[]
yield基本用法
先来了解最基本的用法:
def f():
print("next1")
yield 1
print("next2")
yield 2
return "生成器结束" 作用是:得到异常的报错信息
gen = f()
print("从生成器接收到的值为:", next(gen)) """程序运行到yield 1处停止,并返回yield后面的值"""
print("从生成器接收到的值为:", next(gen)) """此时程序继续向下执行,到yield 2处结束"""
结果:
next1
从生成器接收到的值为: 1
next2
从生成器接收到的值为: 2
可以发现:yield
关键字与return
很相似,都是向外提供数值。但是return
会使整个函数结束,而使用yield
不会,函数会终止,停留在yield处等待下一个"命令"。return
和yield
的关系就像是break
和continue
的关系。
此时如果再调用next(),程序会抛出异常:
StopIteration: 生成器结束
我们还可以对其进行异常处理,得到
try:
next(gen)
except StopIteration as e:
print(e.value)
结果:生成器结束
还可以利用生成器来生成一些有规律的值,如斐波那契数列:
def fib(max):
lst = [0] * max
lst[0], lst[1] = 1, 1
for i in range(2, max):
lst[i] = lst[i-1] + lst[i-2] "当前的值为前两项的和"
yield lst[i]
for num in fib(10):
print(num, end=', ')
得到结果:
2, 3, 5, 8, 13, 21, 34, 55,
yield高级用法
当然yield的功能远不止如此,接下来是yield最牛X的功能,也是最难理解的一个部分。
先通过一段简单的代码来理解:
def gen():
html = yield "http://www.baidu.com" # step2
print(html)
yield "Google"
return 'end'
g = gen()
url = next(g) # step1
print('将url经过下载后得到网页源码"百度"')
print(g.send('百度')) # step3
- 首先生成一个生成器g,程序来到
step1
调用next()
会得到g产生的第一个值,即"http://www.baidu.com"
, 注意此时生成器的内部会来到step2
,等待下次调用时从step2
后开始执行。 step2
的yield它实际上有两个功能 1.向外提供值 2.接收传递进来的值。- 解释器继续向下运行,除了
next()
方法可以让生成器产生值外,生成器g本身也有个send()
方法可以让它“产值”,send()
方法还可以传入值,yield前的变量会接受该值,这里我们模拟一个网页的下载过程,send()
又将下载到的"百度"传入到生成器内部并赋值给html
,此时程序继续向下执行,来到第二个yield
停止。send()
方法又会收到第二个yield
的值,即Google
- 以上如果不理解,手动尝试进行断点调试即可。
理解之后,你就应该感受到yield功能的强大了!协程就可以由yield来实现。下面来看一个生产者消费模型:
def consumer(name):
print('%s已经准备就绪....' % name)
for _ in range(4):
# 1.可以产出值,2,可以接收值(调用方传递进来的值)
fruit = yield "hello" + name # 等待next或是send
print('%s来了,被%s吃了' % (fruit, name))
def producer(name):
c1 = consumer('A')
c2 = consumer('B')
print("nextA的返回值:", next(c1)) # 作用:让生成器走到yield并停止,等同于c1.send(None), 并接收yield发出的值
print("nextB的返回值:", next(c2))
print("循环开始", '-' * 100)
for _ in range(3):
print('%s生产了两种水果' % name)
# 调用send发送非None值之前,必须启动一次生成器,方式有两种1.gen.send(None), 2.next(gen)
print("sendA的返回值:", c1.send('apple')) # 作用 1.回到yield处,并给它赋值 2.跳到下一个yield并接收yield发出的值
print("sendB的返回值:", c2.send('pear'))
print('-' * 100)
if __name__ == '__main__':
producer('aaa')
以上程序可以拷贝至IDE中进行断点调试,重要注释也都写了。