【python高级基础 笔记】多任务-协程
目录
1. 迭代器
- 可以通过for...in...这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象(Iterable),如:list、tuple、str等类型的数据。
- 可以使用 isinstance() 判断一个对象是否是 Iterable 对象(可迭代判断)。如:
from collections import Iterable isinstance([11, 22, 33], Iterable) # 返回True isinstance(100, Iterable) # 返回False
- 可迭代对象的本质就是可以向我们提供一个迭代器(Iterator)帮助我们对其进行迭代遍历使用。
- 一个实现了
__iter__
方法和__next__
方法的对象,就是迭代器。(注:迭代器一定可以迭代,能迭代不一定是迭代器。) - 迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。迭代过程中,每迭代一次(即for...in...每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。在这个过程中迭代器记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。
-
可迭代对象通过
__iter__
方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.。即一个具备了__iter__
方法的对象,就是一个可迭代对象。 -
如果想要一个对象称为一个可以迭代的对象,即可以使用for,那么必须实现__iter__方法。
# 注:for循环执行过程
for temp in xxxx_obj:
# for循环内部代码
1. 判断xxxx_obj是否可以迭代
2. 若 第1步 为可迭代,则调用iter函数,得到xxxx_obj对象的__iter__方法的返回值
3. __iter__方法的返回值是一个迭代器for item in Iterable 循环的本质 :就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。
-
可使用 isinstance() 判断一个对象是否是 Iterator 对象(迭代器判断):
from collections import Iterator isinstance([], Iterator) # 返回False isinstance(iter([11,22]), Iterator) # 返回True
- 可通过iter()函数获取可迭代对象的迭代器。然后可对获取到的迭代器不断使用next()函数来获取下一条数据。iter()函数实际上就是调用了可迭代对象的
__iter__
方法 ,如:li = [11, 22, 33, 44, 55] >>> li_iter = iter(li) >>> next(li_iter) 11 >>> next(li_iter) 22 >>> next(li_iter)
已经迭代完最后一个数据之后,再次调用next()函数会抛出StopIteration的异常。
1.1 实现可迭代的对象 实例1(两个对象)
描述:
要对象能实现for循环,类要实现__iter__方法,该方法返回对象的引用(该对象要有__iter__、__next__方法),for循环的取值是__next__的返回值。
import time
from collections import Iterable
from collections import Iterator
class Classmate(object):
def __init__(self):
self.names = list()
def add(self, name):
self.names.append(name)
def __iter__(self):
"""如果想要一个对象称为一个 可以迭代的对象,即可以使用for,那么必须实现__iter__方法"""
return ClassIterator(self) # 返回对象的引用(对象要有__iter__、__next__方法)
# self把自己的引用传给了ClassIterator对象(即ClassIterator类的对象的init的obj指向了Classmate的对象)
class ClassIterator(object):
def __init__(self, obj):
self.obj = obj
self.current_num = 0 # 记录取第几个
def __iter__(self):
pass
def __next__(self):
if self.current_num < len(self.obj.names):
ret = self.obj.names[self.current_num]
self.current_num += 1
return ret
else:
raise StopIteration # 越界
classmate = Classmate()
classmate.add("张三")
classmate.add("李四")
classmate.add("王五")
# print("判断classmate是否是可以迭代的对象:", isinstance(classmate, Iterable))
# classmate_iterator = iter(classmate)
# print("判断classmate_iterator是否是迭代器:", isinstance(classmate_iterator, Iterator))
# print(next(classmate_iterator))
for name in classmate:
print(name)
time.sleep(1)
1.2 实现可迭代的对象 实例2(一个对象搞定)
import time
from collections import Iterable
from collections import Iterator
class Classmate(object):
def __init__(self):
self.names = list()
self.current_num = 0
def add(self, name):
self.names.append(name)
def __iter__(self):
"""如果想要一个对象称为一个 可以迭代的对象,即可以使用for,那么必须实现__iter__方法"""
return self # 返回自己
# 调用iter(xxobj)的时候 只要__iter__方法返回一个迭代器即可(自己的或别的对象的),
# 但是要保证是一个迭代器(即实现了 __iter__ __next__方法)
def __next__(self):
if self.current_num < len(self.names):
ret = self.names[self.current_num]
self.current_num += 1
return ret
else:
raise StopIteration
classmate = Classmate()
classmate.add("张三")
classmate.add("李四")
classmate.add("王五")
# print("判断classmate是否是可以迭代的对象:", isinstance(classmate, Iterable))
# classmate_iterator = iter(classmate)
# print("判断classmate_iterator是否是迭代器:", isinstance(classmate_iterator, Iterator))
# print(next(classmate_iterator))
for name in classmate:
print(name)
time.sleep(1)
1.3 Fibonacci 斐波那契数列(迭代器)
Fibonacci:
nums = list()
a = 0
b = 1
i = 0 # 第i轮
while i < 10:
nums.append(a)
a, b = b, a+b
i += 1
for num in nums:
print(num)
Fibonacci(用迭代器):用迭代器来实现,每次迭代都通过数学计算来生成下一个数。
class Fibonacci(object):
def __init__(self, all_num):
self.all_num = all_num
self.current_num = 0
self.a = 0
self.b = 1
def __iter__(self):
return self
def __next__(self):
if self.current_num < self.all_num:
ret = self.a
self.a, self.b = self.b, self.a+self.b
self.current_num += 1
return ret # 返回该轮的 a
else:
raise StopIteration
fibo = Fibonacci(10)
for num in fibo:
print(num)
区别:用迭代器返回的是生成数据的方式,不用列表来保存数列的值,什么时候调用,什么时候生成,占用资源少。
注: list、tuple等类型转换也会用到迭代器。
2.生成器
利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器。
2.1 创建生成器
方法1:
第一种方法很简单,只要把一个列表生成式(列表推导式)的 [ ] 改成 ( )。
In [15]:L = [ x*2 for x in range(5)]
In [16]: L
Out[16]: [0, 2, 4, 6, 8]
In [17]: G = ( x*2 for x in range(5))
In [18]: G
Out[18]: <generator object <genexpr> at 0x7f626c132db0>
L 是一个列表,而 G 是一个生成器。可直接打印出列表L的每一个元素。对于G,可按照迭代器的使用方法来使用,即通过next()函数、for循环、list()等方法使用。
for x in G:
print(x)
输出:
0
2
4
6
8
方法2:可以用函数来实现。保证函数有yield,函数自动变成生成器。
生成器实现Fibonacci:
def create_num(all_num):
print("----1---")
# a = 0
# b = 1
a, b = 0, 1
current_num = 0
while current_num < all_num:
print("----2---")
# print(a)
yield a # 如果一个函数中有yield语句,那么这个就不在是函数,而是一个生成器的模板
print("----3---")
a, b = b, a+b
current_num += 1
print("----4---")
# 如果在调用create_num的时候,发现这个函数中有yield那么此时,不是调用函数,而是创建一个生成器对象
obj = create_num(10) # obj即为一个生成器对象
ret = next(obj)
print(ret)
ret = next(obj)
print(ret)
# # 生成器是特殊的迭代器,可迭代,会输出数列
# for num in obj:
# print(num)
输出:
----1---
----2---
0
----3---
----4---
----2---
1
注:
通过以上输出,可以看出,每次调用生成器并非从头开始执行。
第一次 next() 调用时,从头开始执行,输出----1---、----2---,执行到 yield 处,会在此处“暂停”,并返回 a。
此时a = 0 ,输出 0 ;
第二次 next() 调用时,从 yield 处继续执行,输出----3---、----4--- 、----2---,循环回到 yield处“暂停”........
注:可创建多个生成器对象,多个生成器next()调用互不影响。
生成器实现Fibonacci中,将原本在迭代器__next__
方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。只要在 def 中有 yield 关键字的 就称为 生成器。
2.2 通过异常判断生成器结束、获取返回值 实例:
如果想拿到generator的return语句的返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中。
def create_num(all_num):
# a = 0
# b = 1
a, b = 0, 1
current_num = 0
while current_num < all_num:
# print(a)
yield a # 如果一个函数中有yield语句,那么这个就不在是函数,而是一个生成器的模板
a, b = b, a+b
current_num += 1
return "ok...."
obj2 = create_num(8)
while True:
try:
ret = next(obj2)
print(ret)
except Exception as ret:
print(ret.value) # 返回值包含在异常的的value中
break
输出:
0
1
1
2
3
5
8
13
ok....
2.3 send唤醒生成器
除了用next()函数来唤醒生成器继续执行,还可以使用send()函数来唤醒执行。使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据:生成器对象.send(传值) 。
def create_num(all_num):
a, b = 0, 1
current_num = 0
while current_num < all_num:
ret = yield a
print(">>>ret>>>>", ret)
a, b = b, a+b
current_num += 1
obj = create_num(10)
# obj.send(None) # send一般不会放到第一次启动生成器,如果非要这样做 那么传递None
ret = next(obj) # 第一次一般用next来启动
print(ret)
# send里面的数据会 传递给第5行,当做yield a的结果,然后ret保存这个结果,,,
# send的返回结果是下一次调用yield时 yield后面的值 a
ret = obj.send("hello")
print(ret)
执行结果:
0
>>>ret>>>> hello
1
解释:
第一次用next来启动,返回值为yield后面的值a,输出0;
第二次通过send启动,send传的值给了ret,输出>>>ret>>>> hello;
send返回值为yield后面的值a,此时为1,输出1。
2.4 . 生成器小结:
- 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
- yield关键字有两点作用:
- 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
- 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
- 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
- Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)。
3. 协程 - yield (了解
协程,又称微线程,纤程。英文名Coroutine。是python个中另外一种实现多任务的方式,比线程更小占用更小执行单元(理解为需要的资源)。它自带CPU上下文,只要在合适的时机, 可把一个协程切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文,那程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行(非函数调用),且切换的次数以及什么时候再切换到原来的函数都由开发者确定。
yield实现多任务实例:
import time
def task_1():
while True:
print("---1----")
time.sleep(0.1)
yield
def task_2():
while True:
print("---2----")
time.sleep(0.1)
yield
def main():
t1 = task_1()
t2 = task_2()
# 先让t1运行一会,当t1中遇到yield的时候,再返回到24行,然后
# 执行t2,当它遇到yield的时候,再次切换到t1中
# 这样t1/t2/t1/t2的交替运行,最终实现了多任务....协程
while True:
next(t1)
next(t2)
if __name__ == "__main__":
main()
输出:循环输出以下
---1----
---2----
---1----
---2----
---1----
---2----...省略...
解释:while循环内,next(t1)唤醒执行到yield处暂停返回,next(t2)唤醒执行到yield处暂停返回.......
(并发:假的多任务)
4. 协程 - greenlet (了解
为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单。
使用如下命令安装greenlet模块:
sudo pip3 install greenlet
使用greenlet完成多任务:
from greenlet import greenlet
import time
def test1():
while True:
print("---A--")
gr2.switch()
time.sleep(0.5)
def test2():
while True:
print("---B--")
gr1.switch()
time.sleep(0.5)
gr1 = greenlet(test1) # gr1 是全局变量
gr2 = greenlet(test2)
# 切换到gr1中运行
gr1.switch()
输出:
---A--
---B--
---A--
---B--
...省略...
对象.switch() 切换到对象中运行。
5. 协程 - gevent (重点
- 实际中,较少用yield、greenlet,更多的是使用gevent 。
- greenlet已经实现了协程,但还需人工切换。python还有一个比greenlet更强大的并且能够自动切换任务的模块
gevent
。 gevent
原理:当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
安装gevent:
sudo pip3 install gevent
5.1 gevent实现多任务:
import gevent
import time
def f1(n):
for i in range(n):
print(gevent.getcurrent(), i)
# time.sleep(0.5)
gevent.sleep(0.5) # 模拟一个耗时操作,注意不是time模块中的sleep
def f2(n):
for i in range(n):
print(gevent.getcurrent(), i)
# time.sleep(0.5)
gevent.sleep(0.5)
def f3(n):
for i in range(n):
print(gevent.getcurrent(), i)
# time.sleep(0.5)
gevent.sleep(0.5)
print("----1---")
g1 = gevent.spawn(f1, 5) # 创建gevent对象
print("----2---")
g2 = gevent.spawn(f2, 5)
print("----3---")
g3 = gevent.spawn(f3, 5)
print("----4---")
g1.join()
g2.join()
g3.join()
输出:
----1---
----2---
----3---
----4---
<Greenlet at 0x7f9269843748: f1(5)> 0
<Greenlet at 0x7f9269843948: f2(5)> 0
<Greenlet at 0x7f9269843a48: f3(5)> 0
<Greenlet at 0x7f9269843748: f1(5)> 1
<Greenlet at 0x7f9269843948: f2(5)> 1
<Greenlet at 0x7f9269843a48: f3(5)> 1
<Greenlet at 0x7f9269843748: f1(5)> 2
<Greenlet at 0x7f9269843948: f2(5)> 2
<Greenlet at 0x7f9269843a48: f3(5)> 2
<Greenlet at 0x7f9269843748: f1(5)> 3
<Greenlet at 0x7f9269843948: f2(5)> 3
<Greenlet at 0x7f9269843a48: f3(5)> 3
<Greenlet at 0x7f9269843748: f1(5)> 4
<Greenlet at 0x7f9269843948: f2(5)> 4
<Greenlet at 0x7f9269843a48: f3(5)> 4
解释:
- 创建gevent对象时并不会马上执行,调用 对象.join() 时 堵塞等待 对象执行完(延时操作),遇到延时,gevent切换任务开始执行。
- 在函数内部,遇到 gevent.sleep(0.5) 延时才会切换任务,time.sleep(0.5)延时并不会切换任务。
- 协程利用了在等待耗时期间的时间,去做其他事情。
注意到:遇到 gevent.sleep(0.5) 延时才会切换任务,time.sleep(0.5)延时并不会切换任务。如果每次耗时操作都要改成gevent的,会十分麻烦。故需给gevent打补丁。
5.2 给gevent打补丁
有耗时操作时,打补丁方法:
from gevent import monkey
monkey.patch_all() # 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
gevent实现多任务打补丁实例:
import gevent
import time
from gevent import monkey
monkey.patch_all()
def f1(n):
for i in range(n):
print(gevent.getcurrent(), i)
time.sleep(0.5)
def f2(n):
for i in range(n):
print(gevent.getcurrent(), i)
time.sleep(0.5)
def f3(n):
for i in range(n):
print(gevent.getcurrent(), i)
time.sleep(0.5)
# print("----1---")
# g1 = gevent.spawn(f1, 5)
# print("----2---")
# g2 = gevent.spawn(f2, 5)
# print("----3---")
# g3 = gevent.spawn(f3, 5)
# print("----4---")
# g1.join()
# g2.join()
# g3.join()
gevent.joinall([
gevent.spawn(f1, 5),
gevent.spawn(f2, 5),
gevent.spawn(f2, 5)
])
注: 真正调用时,可以不用一个一个对象的调用 对象.join() ,直接用 gevent.joinall() 传入一个创建对象的列表即可。
6. 进程、线程、协程小总结
- 进程是资源分配的单位(一个进程可有多个线程);
- 线程是操作系统调度的单位(一个线程可以有多个协程);
- 进程切换需要的资源很最大,效率很低;
- 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下);
- 协程切换任务资源很小,效率高(网络请求等耗时任务多时,如不考虑GIL可考虑用协程);
- 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发。