多任务编程——协程

本文主讲:Python中关于协程相关的知识……

可迭代对象及其检测方法

为什么学习可迭代对象(迭代器)

答:因为学习迭代器是为了学习生成器,学习生成器是为了学习协程!

什么是可迭代对象

答:可迭代对象就是可以被for循环遍历的对象,因此可迭代也称为可遍历

可迭代(遍历)对象举例:列表、元组、字典、字符串、range()

不可迭代对象举例:自定义类对象(不包括内含迭代器的类对象)、函数、整数(int)

注意事项:遍历字典默认遍历的字典的key值

可迭代判断截图:

image-20211011172657007

判断对象是否可迭代的方法

方法:isinstance(判断对象,Iterable)

判断实例:bool = isinstance(判断对象,Iterable)

注意事项:

1:在利用 isinstance(判断对象,Iterable)方法判断对象是否可迭代时,要先导入collections模块内的 Iterable类!!,即 from collections import Iterable语句 ,否则程序报错

2:Iterable是一个可迭代对象类,isinstance()判断方法就是判断目标对象是否为Iterable类的一个子类

3:判断对象是可迭代对象返回 True,不是返回 False

4:默认的自定义类对象是不可迭代的,但是如果类中含有__iter__( ) 方法,那么这个类创建的对象就是可迭代的,其中__iter__( ) 方法也称为迭代器

快速代码体验

image-20211011172714995

初识迭代器 (iter( )魔法方法)

概念:类默认是不可迭代对象,但是在类中如果含有__iter__( ) 这个方法,那么这个类创建的对象就是可迭代对象,iter( ) 魔法方法就是迭代器

可迭代对象的本质:创建对象的所属类中含有迭代器(iter( )),可向外提供一个迭代器

注意事项:默认类是不可迭代的,如果一个类创建的对象判定为可迭代,那么这个对象所属的类中一定含有__iter__方法

快速代码体验:

image-20211011172803597

迭代器及其使用方法

课前回顾:默认类创建的对象是不可迭代的,如果一个类创建的对象判定为可迭代,那么这个对象所属的类中一定含有__iter__方法(迭代器)

什么是迭代器?

概念:用 for 循环遍历可迭代对象过程中,要有一个“人”来记录遍历访问第几个数据了,并且返回数据,以便于下一次遍历时返回的是下一个数据,这是把帮助我们记录遍历第几个数据的“人”称为迭代器(iterable)

注意事项:

1:类创建的可迭代对象通过__iter__方法向我们提供了一个迭代器,我们在遍历一个可迭代对象时,其实是先获取可迭代对象提供的的迭代器,然后再通过这个迭代器依次获取对象中的每一个数据!

2:比如说列表是一个可迭代对象,那么列表会向外提供一个迭代器,这个迭代器会获取列表内每个数据用于遍历

3:只要是可迭代对象,都可以获取这个对象的迭代器

迭代器特点:

1:记录遍历的位置
2:提供下一个元素的值(配合next(迭代器)函数使用)

循环遍历可迭代对象依次获取数据的原理

列表(可迭代对象)→iter()函数获取迭代器→next(迭代器)函数依次获取迭代器内的数据!(执行几次next函数,获得几个可迭代对象数据)→捕获异常

获取可迭代对象的迭代器函数——iter()

功能:获取可迭代对象的迭代器

语法:iteratro_obj = iter(可迭代对象)
快速代码体验

image-20211011172840216

next()函数

功能:获取迭代器中的下一个元素的数据

语法:print(next(迭代器对象))

注意事项:

1:调用了几次next(迭代器对象)函数,就打印几次迭代器内的数据

2:如果调用next()函数的次数大于迭代器内的数据数量,程序报错

快速代码体验:

image-20211011172920020

for循环遍历可迭代对象的底层原理

第一步:iter(可迭代对象)方法获取可迭代对象的迭代器 即调用可迭代对象类内的__iter__( )方法,iter( )方法返回的是一个迭代器类创建的迭代器

第二步:利用next(迭代器)方法依次获取数据,即多次调用迭代器类里面的__next__( )函数获取数据,直到数据全部获取成功结束迭代

第三步:捕获异常,如果next()函数调用次数大于迭代器内的数据数,程序报错,捕获这个异常防止程序报错

快速代码体验(异常截图):

image-20211011173000787

for循环底层原理截图:

image-20211011173007032

自定义迭代器类

自定义迭代器类注意事项:

1:类里面必须含有__iter( )__方法

2:类里面必须含有 next( )方法

快速代码体验

image-20211011173018655

自定义迭代对象、迭代器【重点、难点】

自定义一个可迭代对象类(列表)

构造必要条件:一个类默认是不可以迭代的,要想让一个类可迭代,类里面必须有 iter 方法(提供迭代器),其中提供的迭代器是另外定义的一个类,迭代器类里面必须包含__iter__ ( )方法与__next__()方法,而且自定义一个列表类,类里面还要有增加数据以及其他的基本方法,还有最起码的 __init__构造方法

自定义可迭代对象类(列表)条件清单:

1:两个类(自定义列表类与自定义迭代器类)

2:列表类里面的__iter__ ( )、init()、基本数据构造等方法

3:迭代器类里面的__iter__ ( )、next( )、init( )方法

注意事项:

1:自定义迭代器类是为自定义列表类的__iter__( )方法提供一个迭代器

条件截图:

image-20211011173038555

自定义列表类代码截图(三段代码组成一段程序)

image-20211011173109740

image-20211011173118610

image-20211011173125582

自定义可迭代对象的方法步骤

第一步:自定义可迭代对象类,内含__init__( )、 iter( )、 其他基本功能方法如添加数据等方法

第二步:自定义一个迭代器类,用来给自定义可迭代对象类提供一个迭代器,内含__init__( )、 iter( )、next( ) 方法

第三步:自定义可迭代对象类的__init( )方法要初始化一个容器保存数据,可以是列表、元组等

第四步:创建的可迭代对象类中的__iter__( )方法要返回创建的迭代类实例化的迭代器

第五步:创建的迭代器类里面的__next__( )方法要返回数据容器存放的数据!每调用一次返回一个数

第六步:创建的迭代器类里面的__next__( )方法要进行抛出异常即在遍历完成时结束迭代

常见问题【重点、重点】

目标:知道 a = b b = a + b 与 a ,b = b , a + b 的区别【重点】斐波那契数列必须知道这两个的区别才能书写代码

代码区别

a = b
b = a + b
上面代码先将 b 的值赋值给 a 在计算 b 的数值

————————————————————————————————————

a ,b = b , a + b

上面代码是先计算等号右面的数值,即 b 与 a + b 的数值,在赋值给等号左面类似于下面的代码

temp = b
b = a + b
a = temp

代码演示

image-20211011173228774

迭代器应用——斐波那契数列【重点】

什么是斐波那契数列?

image-20211011173257483

斐波那契数列实现原理:

image-20211011173306874

创建一个迭代器类实现输出打印斐波那契数列

创建迭代器类满足条件:init( )、 iter( )、 next( )、三个函数

注意事项:

1:迭代器类本身就是一个迭代器,因此迭代器类创建的实例可以被for循环遍历

2:想要迭代器类创建的对象可以被for循环遍历,那么迭代器类里面的__iter__( )方法就要返回迭代器本身,即return self

斐波那契数列代码实现:

image-20211011173407453

image-20211011173416687

掌握斐波那契数列底层代码不能书写的样式

答:底层重要代码不能这么写 a = b b = a + b

生成器——基本使用

什么是生成器(generator)?

概念:生成器是一种特殊的迭代器(按照一定规律生成数列),是一种更加简洁的迭代器,生成器可以理解为可暂停的函数!

创建生成器两种方式:

1:根据列表推导式演变创建生成器 → generator_obj = ( i for i in range(10) )

2:函数中使用了 yield 关键字

注意事项:

1:生成器是特殊迭代器,因此生成器也可以用next(生成器)来获取数据

2:生成器也可以被遍历

快速代码体验(推导式创建生成器)

image-20211011173500784

函数中使用 yield 创建生成器(函数为一个生成器)

image-20211011173507678

生成器案例——斐波那契数列【重点】

利用函数内的 yield 关键字创建斐波那契数列生成器

注意事项:调用一次next(生成器对象)函数,就会执行一次生成器函数到 yield 关键字处返回数据且暂停程序,直到下一次调用next(生成器对象)函数或者send()函数才可以再次唤醒生成器程序函数继续执行!

代码截图

image-20211011173537699

生成器函数内 yield 关键字功能

功能1:返回数据

功能2:暂停函数,不是退出函数,是暂停函数,这也是yield与return的区别!直到下一次调用next()函数时程序继续执行
也可以理解为遇到yield关键字记录位置退出函数,下一次调用next()函数从记录的位置继续执行代码!

image-20211011173547455

生成器使用注意事项【send、return】

函数生成器中 return 的作用

功能:退出生成器函数,不在继续生成数据,当继续执行 next()函数时报错,报错内容就是 return 返回的数据内容,对此我们可以对下面执行的 next()进行异常捕获

注意事项:生成器内的 return 一般要配合 if 条件使用,在满足规定条件后才执行return语句,退出生成器函数

快速代码体验

image-20211011173604573

send 方法启动生成器并且传递参数

send(参数)功能:可以唤醒暂停的生成器程序(生成器内部遇见 yield 关键字就会暂停,直到继续调用next()或send()函数),并且向生成器内传递参数即 参数 = yieid result

使用语法:生成器对象.send(参数)

快速代码体验

image-20211011173816824

协程——基本使用(yield实现协程)

什么是协程

概念:协程可以理解为一种特殊的生成器,在不开辟其他子线程的情况下(只有主线程)可以实现多任务,并且协程有暂停函数的功能,且协程也称为微线程或者纤程

协程适用场景:在程序中存在大量不需要 cpu 的操作时,如I/O操作

线程与协程的差异:在实现多任务时,线程的切换非常耗性能,协程切换没有线程那么耗能

图示:

image-20211011173908875

函数使用yield关键字可以实现协程

实现步骤:

1:创建两个生成器函数(内涵yield)

2:获取这两个生成器

3:value = next(生成器)来获取生成器返回的值

代码演示:

image-20211011173921881

greenlet 实现协程

什么是 greenlet ?

答:greenlet 是 python 的一个 c 扩展,旨在提供一个可自行调度的 “微线程”(协程)方法

安装 greenlet 第三方库指令:pip3 install greenlet

怎么用 greenlet 实现协程多任务(步骤)

第一步:导入 greenlet 模块 from greenlet import greenlet(导入greenlet模块里面的greenlet类)

第二步:创建任务函数(不含yield 必须含有任务切换方法 switch())

第三步:创建协程 greenlet 对象,几个任务函数创建几个协程对象,并为协程对象指定函数任务(函数无括号) 即 协程对象 = greenlet(函数任务)

第四步:手动选择协程先执行哪个任务函数 即 协程对象.switch()

注意事项:

1:创建的任务函数一定要包含 switch() 切换任务函数

2:greenlet实现协程在只有主线程情况下进行多任务

快速代码体验:

gevent 实现协程【重中之重】

什么是 gevent

答:上一集我们用 greenlet 实现了协程多任务,但是多任务却要我们手动切换任务函数,不要捉急 python 有一个更为强大的第三方库 gevent 它可以实现自动切换任务函数

gevent 第三方库可以自动检测代码中哪些是耗时操作,遇到耗时较长的代码就会立刻切换到另一个任务去执行,然后在一个合适的时间返回执行刚开始的函数

gevent 怎么使用及实现协程多任务

gevent实现多任务步骤:

1:导入模块 import gevent

2:创建任务函数,任务函数内含耗时操作(gevent.sleep(0.5))

3:为 gevent 指派任务 并且返回一个 gevent 对象 即 对象 = gvenv.spawn(任务函数,参数1,参数2,…….),注意有几个任务就指派几次任务返回几个对象,先指派的哪个任务就先执行哪个任务函数

4:让主线程等待协程执行结束后在结束,即 gevent对象.join()

注意事项:

1:在任务函数中耗时操作为什么不能使用 time.sleep(0.5)只能使用 gevent.sleep(0.5),因为gevent只能识别自己的gevent.sleep(0.5)是耗时操作,不能识别time.sleep(0.5)是耗时操作,但是它们两个的效果是一样的

2:第四步的join()必须书写,否则在协程没有执行结束,主线程就结束了并且释放数据和内存,不能输出任何数据

3:耗时操作包括 I/O操作,比如文件的读取写入

快速代码体验:

给一个 gevent 不能识别为耗时操作的代码(time.sleep(0.5)),转变成可以识别为耗时代码操作(打补丁)

什么是打补丁:打补丁就是在不修改程序源代码的情况下为程序增加新的功能

给 gevent 打补丁:在不改变 gevent 原始代码的情况下,为 gevent 增加新的功能

利用打补丁操作将 gevent 不能识别为耗时操作的代码(time.sleep(0.5))识别为耗时操作步骤:

第一步:导入模块 from gevent import monkey

第二步:破解所有即可 monkey.patch_all( ) 这样就可以识别 time.sleep(0.5)代码为耗时操作了

注意事项:上面的这两行代码一般都写在一个程序的最上方!

快速代码体验:

image-20211011174538875

猴子补丁功能:
1:在不修改第三方源代码的情况下增加原来不支持的功能
2:在运行时为内存中的对象增加补丁,而不是在磁盘的源代码中增加补丁

线程、进程、协程区别【重点】

进程、线程、协程

image-20211011174623986

进程、线程、协程关系

image-20211011174631826

线程、进程、协程应用场景

多进程:密集 cpu 任务,需要充分使用多核cpu资源
缺点:多个进程间通信成本高、切换开销大

多线程:密集 I/O任务(网络I/O,磁盘I/O,数据库I/O) 网络I/O即联网下载上传文件
缺点:同一个时间切片只能运行一个线程,不能做到高并行,能做到高并发

多协程:不需要大量 cup 操作时,应用于网络 I/O 下载时
缺点:单线程执行,处理大量cpu操作以及磁盘I/O操作时性能较低,但是处理网络I/O性能比较高

注意事项:性能最高的组合即进程+协程,进程可调度多个cpu

案例:并发下载器

urllib.request 第三方库的 urlopen(url) 方法

功能:打开指定 url 的网站并且返回一个类文件,这个类文件支持一般文件的操作比如说read()、readlin()等

注意事项:类文件对象 = urllib.reques.urlopen(url)返回一个类文件,但是要对这个类文件执行read()方法才能读取到其中的数据从而实现保存文件的结果

具体功能详解:https://www.cnblogs.com/langdashu/p/4963053.html

使用步骤:

第一步:导入模块 import urllib.request

第二步:返回类文件对象,类文件对象 = urllib.reques.urlopen(url)

第三步:对这个类文件对象进行操作(读取这个文件数据) 即:类文件对象.read()等操作

图示:

image-20211011174757294

给所有的 gevent 协程任务 join()的方法 gevent.joinall()

功能:给所有指定任务的协程进行 join()

语法:gevent.joinall([协程任务1,协程任务2,协程任务3])

图示:

image-20211011174805756

协程并发下载多种图片

原理图示:

image-20211011175004393

实现并发下载器的步骤:

第一步:导入猴子补丁模块并且破解所有 即 from gevent import monkey monkey.patch()

第二步:导入urllib.request 与 gevent 模块

第三步:定义down_load()函数用于下载内容

1:获取图片的类文件
2:打开本地文件写入图片的二进制文件
3…

第四步:定义 main()函数保存下载内容的 url 网站 并且调用 down_load( )函数对文件进行下载

第五步:对 down_load( )函数进行异常捕获

代码演示:

image-20211011175102556

案例:web协程服务器

略……

未完待续….