【python进阶 笔记】多任务-协程(迭代器、生成器、yield、greenlet 、gevent)

【python高级基础 笔记】多任务-协程

目录

1. 迭代器

1.1 实现可迭代的对象 实例1(两个对象)

1.2 实现可迭代的对象 实例2(一个对象搞定)

1.3 Fibonacci 斐波那契数列(迭代器)

2.生成器

2.1 创建生成器

2.2 通过异常判断生成器结束、获取返回值 实例:

2.3 send唤醒生成器

2.4 . 生成器小结:

3. 协程 - yield (了解

4. 协程 - greenlet (了解

5. 协程 - gevent (重点

5.1 gevent实现多任务:

5.2 给gevent打补丁

6. 进程、线程、协程小总结


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. 进程、线程、协程小总结

  1. 进程是资源分配的单位(一个进程可有多个线程);
  2. 线程是操作系统调度的单位(一个线程可以有多个协程);
  3. 进程切换需要的资源很最大,效率很低;
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下);
  5. 协程切换任务资源很小,效率高(网络请求等耗时任务多时,如不考虑GIL可考虑用协程);
  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发。
发布了50 篇原创文章 · 获赞 10 · 访问量 6617

猜你喜欢

转载自blog.csdn.net/qq_23996069/article/details/104063611