生成器、协程
生成器
9.1.1 生成器的本质 ★★★★
在 Python 中,使用了 yield 的函数被称为生成器(generator)。
跟普通函数不同,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器本质上就是一个迭代器。
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
示例如下:
def fei(n):
num1, num2 = 0, 1
current = 0
while current < n:
num = num1
num1, num2 = num2, num1 + num2
current += 1
yield num
注意事项:
调用一个生成器函数,返回的是一个迭代器对象。
生成器的构造方法
方法一:
# 列表推导式
ret_01 = [i for i in range(1,11)]
print(ret_01)
控制台显示为:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 生成器
ret_02 = (i for i in range(1,11))
print(ret_02)
控制台显示为:
<generator object <genexpr> at 0x0000013DE77B39E8> # generator 生成器对象
总结:将列表推导式的 [ ] 改成 ( ) 后,就是一个generator生成器对象
方法二:
在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。简单来说:只要在def中有yield关键字的 就称为 生成器
示例如下:
def fei(n):
num1, num2 = 0, 1
current = 0
while current < n:
num = num1
num1, num2 = num2, num1 + num2
current += 1
yield num
if __name__ == '__main__':
ret = fei(10)
# 查看生成器的第一种方式next()
# 查看生成器的第二种方式强转,例如:list()、tuple()
# 查看生成器的第三种方式for循环
StopIteration停止迭代异常
def fei(n):
num1, num2 = 0, 1
current = 0
while current < n:
num = num1
num1, num2 = num2, num1 + num2
current += 1
yield num
if __name__ == '__main__':
ret = fei(10)
# 查看生成器的第一种方式next()
while True:
try:
print(next(ret))
except StopIteration:
break
# 查看生成器的第二种方式强转,例如:list()、tuple()
# 查看生成器的第三种方式for循环
yield关键字讲解
yield关键字作用:
使用了yield关键字的函数不再是函数,而是生成器。(即:使用了yield的函数就是生成器)
yield关键字有两点作用:
保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return返回值的作用
可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)
协程的介绍
协程的概念描述
协程: 微线程,也称为用户级线程,在不开辟线程的基础上完成多个任务按照一定顺序交替执行
如何理解协程: 在def里面只看到一个yield关键字就是协程
协程的优势
优势01:协程最大的优势就是极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。(内部封装了cpu上下文)
优势02:不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
相比于进程和线程,协成的特点:协成可以在单个线程的基础上多个任务交替执行,所以协程切换任务资源很小,效率高
基本协程的构造
在def里面只看到一个yield关键字就是协程(实际上也可以当成生成器来看)
import time
# 定义协程1
def work_1():
while True:
print("work1...")
time.sleep(0.1)
yield
# 定义协程2
def work_2():
while True:
print("work2...")
time.sleep(0.1)
yield
# 创建协程
g1 = work_1()
g2 = work_2()
while True:
next(g1)
next(g2)
通过yield/send方式实现生产者/消费者模型:
def consumer(): # consumer 消费者
r = ''
while True:
n = yield r
print('[CONSUMER] Consuming %s...' % n)
def produce(c): # produce 生产者
c.send(None)
for n in range(1, 6):
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
c.close()
c = consumer()
produce(c)
greenlet模块
greenlet模块讲解
为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单
安装方式:
使用如下命令安装greenlet模块:
pip3 install greenlet
使用greenlet模块演示协程
案例演示:
import greenlet
import time
# greenlet封装的是yield,为了让程序员更好的使用协程
# 任务1
def work1():
for i in range(5):
print("work1....")
time.sleep(0.3)
# 切换到协程2执行对应的任务
g2.switch()
# 任务2
def work2():
for i in range(5):
print("work2....")
time.sleep(0.3)
# 切换到协程1执行对应的任务
g1.switch()
# 创建协程指定对应的任务
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)
# 启动并切换到指定协程执行对应的任务
g1.switch()
使用greenlet实现生产者/消费者模型:
from greenlet import greenlet
def consumer(): # 消费者 - consumer
while True:
n = gr_produce.switch()
print('[CONSUMER] Consuming %s...' % n)
def produce(): # 生产者 - produce
for n in range(1, 6):
print('[PRODUCER] Producing %s...' % n)
gr_consumer.switch(n)
gr_consumer = greenlet(consumer)
gr_produce = greenlet(produce)
#切换到consumer中运行
gr_consumer.switch()
gevent模块
gevent模块简介
greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
安装方式
使用如下命令安装gevent模块:
pip3 install gevent
使用gevent模块演示协程
gevent基本使用方法介绍:
- gevent.spawn(func, *args, …)方法用来生成协程,他接受一个函数作为参数
- gevent.joinall([t1, t2, …])方法用来启动协程轮询并等待运行结果
- gevent.sleep()用来模拟一个IO操作,阻塞后自动切换到另一个协程执行
gevent模块案例演示:
from gevent import monkey
# 打补丁,让gevent能够识别系统的耗时操作和网络请求延时操作
# 提示: 这行代码一定要先执行。
monkey.patch_all()
#总结: gevent封装的是greentlet, gevent可以完成协程之间自动切换
#任务1
def work1():
# 获取当前任务执行的协程对象
print("work1:", gevent.getcurrent())
for i in range(10):
print("work1")
time.sleep(0.1)
# gevent.sleep(0.1)
# 任务2
def work2():
# 获取当前任务执行的协程对象
print("work2:", gevent.getcurrent())
for i in range(10):
print("work2")
time.sleep(0.1)
# gevent.sleep(0.1)
# 创建协程指定对应的任务
g1 = gevent.spawn(work1)
g2 = gevent.spawn(work2)
# 让主线程等待协程执行完成以后程序再退出
g1.join()
g2.join()
# 方式2
gevent.joinall([g1, g2])
gevent综合案例
从网页中进行图片抓取:
import gevent
import urllib.request # 网络请求模块
from gevent import monkey
# 打补丁, 让gevent识别耗时操作
monkey.patch_all()
# 下载图片
def download_img(img_url, img_name):
try:
# 请求打开网络资源
response = urllib.request.urlopen(img_url)
with open(img_name, "wb") as img_file:
while True:
# 读取请求的资源数据
img_data = response.read(1024)
if img_data:
# 写入到指定图片名里面
img_file.write(img_data)
else:
break
except Exception as e:
print("下载图片出现异常:", e)
else:
print("%s图片下载成功" % img_name)
if __name__ == '__main__':
# 准备图片地址
img_url1 = "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3941403894,1764478959&fm=26&gp=0.jpg"
img_url2 = "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1481212717,1253269835&fm=27&gp=0.jpg"
img_url3 = "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=4190184537,2697009061&fm=27&gp=0.jpg"
# 主线程等待所有的协程执行完成以后程序再退出
gevent.joinall([
# 创建协程并指派任务
gevent.spawn(download_img, img_url1, "1.jpg"),
# 创建协程并指派任务
gevent.spawn(download_img, img_url2, "2.jpg"),
# 创建协程并指派任务
gevent.spawn(download_img, img_url3, "3.jpg")
])