Python-多任务-协程

Python-协程

一 迭代

  • 迭代:是访问集合元素的一种方式。我们已经知道可以对list、tuple、str等类型的数据使用for...in...的循环语法从其中依次拿到数据进行使用,我们把这样的过程称为遍历,也叫迭代

1 可迭代对象

  • 可迭代对象(Iterable):我们把可以通过for...in...这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象(Iterable);可迭代对象的本质就是可以向我们提供一个中间“人”即迭代器帮助我们对其进行迭代遍历使用。可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据

2 迭代器

  • 迭代器(Iterator):是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。我们分析对可迭代对象进行迭代使用的过程,发现每迭代一次(即在for...in...中每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。那么,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们把这个能帮助我们进行数据迭代的“人”称为迭代器(Iterator)

3 如何判断一个对象是否可以迭代

  • 可以使用 isinstance() 判断一个对象是否是 Iterable 对象:

  • isinstance() 与 type() 区别:

  • type() 不会认为子类是一种父类类型,不考虑继承关系。

  • isinstance() 会认为子类是一种父类类型,考虑继承关系。

如果要判断两个类型是否相同推荐使用 isinstance()。

class A:
    pass


class B(A):
    pass


print(isinstance(A(), A))  # returns True
print(type(A()) == A)  # returns True
print(isinstance(B(), A))  # returns True
print(type(B()) == A)  # returns False  考虑的是这个对象是由谁造出来的
from collections import Iterable

for i in "abc":
    print(i)


class myList(list):
    pass


print(isinstance([], Iterable))  # True

print(isinstance({}, Iterable))  # True

print(isinstance('abc', Iterable))  # True

print(isinstance(myList, Iterable))  # False
print(isinstance(list, Iterable))  # False
print(isinstance(list(), Iterable))  # True
print(isinstance(myList(), Iterable))  # True

print(isinstance(100, Iterable))  # False
  • 可迭代对象的本质

    我们分析对可迭代对象进行迭代使用的过程,发现每迭代一次(即在for...in...中每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。那么,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们把这个能帮助我们进行数据迭代的“人”称为迭代器(Iterator)

    可迭代对象的本质就是可以向我们提供一个这样的中间“人”即迭代器帮助我们对其进行迭代遍历使用。

    可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.

    那么也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象。

from collections import Iterable


class MyList():
    def __init__(self):
        self.container = []

    def add(self, item):
        self.container.append(item)

    def __iter__(self):  # 一个具备了__iter__方法的对象,就是一个可迭代对象
        pass


mylist = MyList()

print(isinstance(mylist, Iterable))  # True

4 iter()函数与next()函数

iter()获取这些可迭代对象的迭代器。iter()函数实际上就是调用了可迭代对象的__iter__方法。

next()对获取到的迭代器不断使用next()函数来获取下一条数据

li = [1, 2, 3, 4, 5]

# print(next(li))  # 报错:TypeError: 'list' object is not an iterator

li_iter = iter(li)

print(next(li_iter))  # 1
print(next(li_iter))  # 2
print(next(li_iter))  # 3
print(next(li_iter))  # 4
print(next(li_iter))  # 5
print(next(li_iter))  # 报错:StopIteration

print(li.__iter__().__next__())  # 这样不行,会一直是第一个值

# iter()函数实际上就是调用了可迭代对象的__iter__方法。
iter2 = li.__iter__()  # 要获取到__iter__()方法返回的迭代器
print(next(iter2))  # 1
print(next(iter2))  # 2
print(next(iter2))  # 3
print(next(iter2))  # 4

# 实际上,在使用next()函数的时候,调用的就是迭代器对象的__next__方法
print(iter2.__next__())  # 5
print(iter2.__next__())  # 报错:StopIteration
print(iter2.__next__())
print(iter2.__next__())
print(iter2.__next__())
print(iter2.__next__())

5 迭代器Iterator

迭代器一个实现了__iter__方法和__next__方法的对象,就是迭代器。

  • 迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据
  • 实际上,在使用next()函数的时候,调用的就是迭代器对象的__next__方法(Python3中是对象的__next__方法,Python2中是对象的next()方法)。所以,我们要想构造一个迭代器,就要实现它的__next__方法
  • 但这还不够,python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现__iter__方法,而__iter__方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的__iter__方法返回自身即可。
from collections import Iterable


# 创建一个迭代器类
class MyIterator():
    def __init__(self, iterable):
        self.iterable = iterable

        self.current = 0  # 用来访问当前访问到的位置

    def __next__(self):
        if self.current < len(self.iterable.mylist):
            item = self.iterable.mylist[self.current]

            self.current += 1
            return item

        else:
            raise StopIteration

    def __iter__(self):
        return self


# 创建一个可迭代对象类
class MyList():
    def __init__(self):
        self.mylist = []

    def add(self, item):
        self.mylist.append(item)

    def __iter__(self):
        myiterator = MyIterator(self)
        return myiterator


if __name__ == '__main__':
    mylist = MyList()

    print(isinstance(mylist, Iterable))  # True

    mylist.add(1)  # 1
    mylist.add(2)  # 2
    mylist.add(3)  # 3
    mylist.add(4)  # 4

    for num in mylist:
        print(num)

6 如何判断一个对象是否是迭代器

可以使用 isinstance() 判断一个对象是否是 Iterator 对象:

from collections import Iterator

print(isinstance([], Iterator))  # False

print(isinstance(iter([]), Iterator))  # True

print(isinstance(iter("abc"), Iterator))  # True

7 for...in...循环的本质

for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

8 迭代器的应用场景

案例1:

比如第一张表:返回的是 List集合,是因为对于第一个后台的人可能返回List集合的话,对他来说是最方便的,但是如果你非要让他转为set集合再返回给你,可能对于他来说会比较麻烦;
同样的,对于第二张表:返回的是 Set,是因为 返回Set集合对他来说可能比较方便,但是如果你非要让他转为 List集合再返回给你,可能对于他来说会比较麻烦;
同样的,对于第三张表:返回的是 Object[]数组,是因为 返回Object[]数组对他来说可能比较方便,但是如果你非要让他转为 List集合再返回给你,可能对于他来说会比较麻烦;

基于这种情况,这个时候有2种解决方案:
A:第一种:在开发之前大家都必须商量好,返回那种数据类型,这样每个人都必须返回同一种类型就可以,但是这样可能灵活度不够,就可以采用下边第二种方案;
B:第二种:什么都不要说,直接返回迭代器就可以,意思就是你不需要管我返回的是List集合、Set集合、还是Object[]数组,我会把这些都转为迭代器,并且保证你app获取后台数据返回的是迭代器就ok,你不需要管后台接口是如何实现的;

案例2:

我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。

举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

现在我们想要通过for...in...循环来遍历迭代斐波那契数列中的前n个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

9 并不是只有for循环能接收可迭代对象

除了for循环能接收可迭代对象,list、tuple等也能接收。

li = list(Iterator())
print(li)
tp = tuple(Iterator())
print(tp)

二 生成器

利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。

为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)

生成器(generator)生成器是一类特殊的迭代器简单来说:只要在def中有yield关键字的 就称为 生成器

1 创建生成器方法

方法1:

li = [x*2 for x in range(10)]
print(li)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

g = (x*2 for x in range(10))
print(g)  # <generator object <genexpr> at 0x0000000001F01E08>

# 而对于生成器G,我们可以按照迭代器的使用方法来使用,即可以通过next()函数、for循环、list()等方法使用。
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))

方法2:

def fib(n):
    current = 0
    num1, num2 = 0, 1
    while current < n:
        num = num1
        num1, num2 = num2, num1+num2
        current += 1
        yield num
    return 'done'


g = fib(5)

print(g)  # <generator object fib at 0x0000000002941E08>
for num in g:
    print(num)

# 用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中
t = iter(g)
print(next(t))  # 0
print(next(t))  # 1
print(next(t))  # 2
print(next(t))  # 3
print(next(t))  # StopIteration: done
"""
Traceback (most recent call last):
  File "E:/project/test/main.py", line 21, in <module>
    print(next(t))
StopIteration: done
"""

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。简单来说:只要在def中有yield关键字的 就称为 生成器

此时按照调用函数的方式( 案例中为F = fib(5) )使用生成器就不再是执行函数体了,而是会返回一个生成器对象( 案例中为F ),然后就可以按照使用迭代器的方式来使用生成器了。

总结
  • 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
  • yield关键字有两点作用:
    • 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
    • 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
  • 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
  • Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)。

2 使用send、next、__next__()方法唤醒

我们除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据。注意:在一个生成器函数未启动之前,是不能传递数值进去。必须先传递一个None进去或者调用一次next(g)方法,才能进行传值操作。

def fib(n):
    current = 0
    num1, num2 = 0, 1
    while current < n:
        num = num1
        num1, num2 = num2, num1+num2
        current += 1
        data = yield num
        print("通过send传入的数据为:", data)
    return 'done'


g = fib(5)

# print(g.send("haha"))  # TypeError: can't send non-None value to a just-started generator
print(next(g))
print(next(g))
print(g.__next__())
print(next(g))
print(g.send("haha"))
print(g.__next__())  # StopIteration: done

三 协程

协程(Coroutine):又称微线程,纤程。英文名。

  • 协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。 为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
  • 通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定

协程和线程差异

  • 在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

1 协程-yield

import time


def work1(num):
    for _ in range(num):
        print("----work1---")
        yield
        time.sleep(1)


def work2(num):
    for _ in range(num):
        print("----work2---")
        yield
        time.sleep(1)


def main():
    w1 = work1(5)
    w2 = work2(5)

    work_1 = 1
    work_2 = 1
    while work_1 and work_2:
        try:
            next(w1)
        except StopIteration as e:
            work_1 = 0

        try:
            next(w2)
        except StopIteration as e:
            work_2 = 0


if __name__ == "__main__":
    start_time = time.time()
    main()
    end_time = time.time()
    print("总共时间:", end_time-start_time)
"""
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
总共时间: 10.140017986297607
"""

2 协程-greenlet

import time

from greenlet import greenlet


def work1(num):
    for i in range(num):
        print("work1-------", i)
        w2.switch(5)  # 在程序中人为的转换执行代码块
        time.sleep(1)


def work2(num):
    for i in range(num):
        print("work2---", i)
        w1.switch(5)
        time.sleep(1)


if __name__ == "__main__":
    start_time = time.time()
    w1 = greenlet(work1)
    w2 = greenlet(work2)
    w1.switch(5)  # 先从w1开始运行
    end_time = time.time()
    print("总共时间:", end_time-start_time)

"""
work1------- 0
work2--- 0
work1------- 1
work2--- 1
work1------- 2
work2--- 2
work1------- 3
work2--- 3
work1------- 4
work2--- 4
总共时间: 9.12601613998413
"""

3 协程-gevent

协程-gevent:有人在greenlet的基础上,使用心得技术对它进行二次封装

  • greenlet已经实现了协程,但是这个还的人工切换,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
  • 其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
  • 由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
  • greenlet和gevent的切换原理有些不同:greenlet:手动切换;gevent:自动切换(前提是有耗时操作)
  • gevent协程不用调用,直接开始,但是如果主线程没有耗时操作,直接执行完毕,不会有机会切换到例程中的两个协程中,所以要加join()

没有io阻塞的时候

import time

import gevent


def work1(num):
    for i in range(num):
        print("work1-------", i)
        time.sleep(1)


def work2(num):
    for i in range(num):
        print("work2---", i)
        time.sleep(1)


if __name__ == "__main__":
    start_time = time.time()

    w1 = gevent.spawn(work1, 5)
    w2 = gevent.spawn(work2, 5)
    w1.join()
    w2.join()

    end_time = time.time()
    print("总共时间:", end_time-start_time)

"""
work1------- 0
work1------- 1
work1------- 2
work1------- 3
work1------- 4
work2--- 0
work2--- 1
work2--- 2
work2--- 3
work2--- 4
总共时间: 10.280417919158936
"""

有io阻塞的时候

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

import time

import gevent


def work1(num):
    for i in range(num):
        print("work1-------", i)
        # 用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)


def work2(num):
    for i in range(num):
        print("work2---", i)
        # 用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)


if __name__ == "__main__":
    start_time = time.time()

    w1 = gevent.spawn(work1, 5)
    w2 = gevent.spawn(work2, 5)
    w1.join()
    w2.join()

    end_time = time.time()
    print("总共时间:", end_time-start_time)
    
"""
work1------- 0
work2--- 0
work1------- 1
work2--- 1
work1------- 2
work2--- 2
work1------- 3
work2--- 3
work1------- 4
work2--- 4
总共时间: 5.148008823394775
"""

有io阻塞如果使用greenlet

import time

from greenlet import greenlet
import gevent


def work1(num):
    for i in range(num):
        print("work1-------", i)
        w2.switch(5)
        # 用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)


def work2(num):
    for i in range(num):
        print("work2---", i)
        w1.switch(5)
        # 用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)


if __name__ == "__main__":
    start_time = time.time()
    w1 = greenlet(work1)
    w2 = greenlet(work2)
    w1.switch(5)
    end_time = time.time()
    print("总共时间:", end_time-start_time)
    
"""
work1------- 0
work2--- 0
work1------- 1
work2--- 1
work1------- 2
work2--- 2
work1------- 3
work2--- 3
work1------- 4
work2--- 4
总共时间: 9.204015970230103
"""

使用gevent出现的问题

  • 1 不是异步实现,还是主线程推动
  • 2 不识别其他的耗时操作,如(sellp/recv/recvfrom/accept。。。)

解决方案

  • 打补丁
  • 打补丁后出现的问题:input()阻塞还是不能切换协程;所以涉及到input尽量使用线程/进程
import time

import gevent

# 打补丁
from gevent import monkey
monkey.patch_all()


def work1(num):
    for i in range(num):
        print("work1-------", i)
        # 用来模拟一个耗时操作,是time模块中的sleep也可以了
        time.sleep(1)


def work2(num):
    for i in range(num):
        print("work2---", i)
        # 用来模拟一个耗时操作,是time模块中的sleep也可以了
        time.sleep(1)


if __name__ == "__main__":
    start_time = time.time()

    w1 = gevent.spawn(work1, 5)
    w2 = gevent.spawn(work2, 5)
    w1.join()  # 主协程没有还是操作的话,不会进到子协程里面
    w2.join()

    end_time = time.time()
    print("总共时间:", end_time-start_time)
    
"""
work1------- 0
work2--- 0
work1------- 1
work2--- 1
work1------- 2
work2--- 2
work1------- 3
work2--- 3
work1------- 4
work2--- 4
总共时间: 5.078941106796265
"""

四 进程、线程、协程对比

请仔细理解如下的通俗描述

  • 有一个老板想要开个工厂进行生产某件商品(例如剪子)
  • 他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程
  • 只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程
  • 这个老板为了提高生产率,想到3种办法:
    1. 在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程方式
    2. 老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程方式
    3. 老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式
  1. 进程是资源分配的单位
  2. 线程是操作系统调度的单位
  3. 进程切换需要的资源很最大,效率很低
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  5. 协程切换任务资源很小,效率高
  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

五 总结

  1. 我们把数据使用for循环语句从集合元素中依次拿出来的过程成为遍历,也叫迭代,迭代是访问集合元素的一种方式;
  2. 迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。并且迭代器只能往前不会后退;
  3. 从形式上来说一个具备了__iter__方法的对象,就是一个可迭代对象。实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据,一个实现了__iter__方法和__next__方法的对象,就是迭代器
  4. 我们可以使用 isinstance()来判断一个对象是否是迭代对象或者是否是迭代器;
  5. for循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束
  6. 生成器(generator)生成器是一类特殊的迭代器。简单来说:只要在def中有yield关键字的 就称为 生成器;他可以保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起并且将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用;我们除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据
  7. 实现协程的方式有:yield、greenlet、gevent、asyncio
posted @   陈俊明  阅读(119)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示