由浅入深走进Python异步编程【协程与yield】(含代码实例讲解 || 迭代器、生成器、协程、yield from)

发布时间:2023年12月28日

写在前面

从底层到第三方库,全面讲解python的异步编程。这节讲述的是python异步编程的底层原理第一节,详细了解需要配合下一节观看哦。纯干货,无概念,代码实例讲解。

本系列有6章左右,点击头像或者专栏查看更多内容,陆续更新,欢迎关注。

部分资料来源及参考链接:
https://www.bilibili.com/video/BV1Li4y1j7RY/
https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B

同步与异步

同步必须顺序执行,异步可以大家一起执行。但是同步由于是顺序执行,结果是有序的,异步的结构是无序的。

迭代与遍历

迭代常用于数组,列表,元组等有序结构;遍历则常用于二叉树,字典等无序结构。

为什么是这样呢?
这是因为迭代有一个next(见《数据结构》),这个next是有指向性的,但是遍历就是无序搜索

重点:

  1. 迭代比遍历要快
  2. 遍历可以强行用在数组,列表等有序结构,但迭代不能搜索无序结构
  3. (这就是为啥很多老师喜欢把迭代叫成遍历,这是一种误导初学者)

迭代器(iter)

你可以把任何有序的类型,转换为可迭代对象,就像这样

list_data = [0,1,2,3,4,5,6,7,8,9]

iterator_data = iter(list_data)#将可迭代对象 转成 迭代器
print(type(iterator_data))

你也可以使用next方法了,

print(type(iterator_data))

那么,如何创建迭代器呢?

迭代器具有特殊的__iter__,__next__方法

class Numbers:

    def __init__(self):
        self.a = 1

    def __iter__(self):#必须返回自己
 
        return self
 
    def __next__(self):
        data = self.a
        self.a+= 1
        return data

#'__iter__和__next__同时出现就是迭代器了,普通类并没有这两个方法'
NumBer = Numbers()

print(next(NumBer))
print(next(NumBer))
print(next(NumBer))

iter方法写法是固定的,next魔法方法会在使用next时自动调用。

生成器(generator)

相比迭代器,它多了一个yield,而这个关键字可以实现函数状态的挂起与恢复。用以下的代码来体会一下这个含义

def get_data():
    
    list_data = [0,1,2,3,4,5,6,7,8,9]
    for data in list_data:
        return data     #直接就终止了函数
        #yield data    #直接挂起了函数,等待下次恢复

generator_data = get_data()
print(generator_data)
print(type(get_data()))

在return的状态下,马上就会返回并退出函数。但是如何得到我们想要的结果呢,每一次都返回一个迭代出来的值,这就是yield,改成下面的代码:

def get_data():
    
    list_data = [0,1,2,3,4,5,6,7,8,9]
    for data in list_data:
        #return data     #直接就终止了函数
        yield data    #直接挂起了函数,等待下次恢复

generator_data = get_data()
print(generator_data)
print(type(get_data()))

输出结果为
在这里插入图片描述
返回了一个生成器,它是可以使用next方法来获取值的,执行print(next(generator_data))就输出了一个0,再次执行,就会一个个输出。

当然你也可以使用for来进行输出。

所以,其实这里就是yield与循环的巧妙应用。工作流程就像这样:

1.  方法中携带了yield关键字,首次调用时,会返回generator生成器。
2.  首次使用next,生成器激活,开始执行并在yield关键字位置返回,同时挂起当前函数状态。
3.  再次使用next,保存了之前的状态,从yield的下一行开始执行。

协程(coroutine)

协程是一个非常重要的概念。对于理解后续golang中的协程goroutine底层原理有很大帮助。这里先解释coroutine

对于协程,wiki百科有这样的信息

协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。

生成器,也叫作“半协程”,是协程的子集。尽管二者都可以yield多次,挂起(suspend)自身的执行,并允许在多个入口点重新进入,但它们特别差异在于,协程有能力控制在它让位之后哪个协程立即接续它来执行,而生成器不能,它只能把控制权转交给调用生成器的调用者。在生成器中的yield语句不指定要跳转到的协程,而是向父例程传递返回值。

(上述信息的参考链接如下)
https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B

通过上述信息,我们可以知道:协程是允许被挂起与被恢复的;协程是可以通过生成器实现的。再回到生成器的next方法,它做了什么呢?

next

官方文档是这样说的

generator.next()
开始一个生成器函数的执行或是从上次执行 yield 表达式的位置恢复执行。 当一个生成器函数通过 next() 方法恢复执行时,当前的 yield 表达式总是取值为 None。 随后会继续执行到下一个 yield 表达式,这时生成器将再次挂起,而 expression_list 的值会被返回给 next() 的调用方。 如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。

此方法通常是隐式地调用,例如通过 for 循环或是内置的 next() 函数。

(上述信息的参考链接如下)
https://docs.python.org/zh-cn/3/reference/expressions.html?highlight=send#generator.send

现在来调试一段代码,体会一下吧
在这里插入图片描述
此时没有输出。data是一个被挂起的生成器。这就说明,yield可以类似于return一样,终止函数运行。

接下来调用next方法
在这里插入图片描述
进入函数内部,同时指针指向‘挂起了’,yield即刻返回,没有给a赋值,也没有执行函数中的print语句
在这里插入图片描述
继续执行,再次跳入函数
在这里插入图片描述
此时再次恢复函数,执行print语句,但是a仍旧没有赋值
在这里插入图片描述
这里,没有产生下一个值,所以返回了stopiteration
在这里插入图片描述

现在,再倒回去看官方文档的解释就非常清楚了。

开始一个生成器函数的执行或是从上次执行 yield 表达式的位置恢复执行。 当一个生成器函数通过 next() 方法恢复执行时,当前的 yield 表达式总是取值为 None。 随后会继续执行到下一个 yield 表达式,这时生成器将再次挂起,而 expression_list 的值会被返回给 next() 的调用方。 如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。

send

还需要了解一个重要的send方法,其实就是前文中有一句 当一个生成器函数通过 __next__() 方法恢复执行时,当前的 yield 表达式总是取值为 None,而send方法可以更改这个取值的结果,就像这样
在这里插入图片描述
此时的程序结果就是
在这里插入图片描述
这里下面当然是报出了StopIteration错误,因为和next一样的,没有下一个值了。

官方文档解释:

恢复执行并向生成器函数“发送”一个值。 value 参数将成为当前 yield 表达式的结果。 send() 方法会返回生成器所产生的下一个值,或者如果生成器没有产生下一个值就退出则会引发 StopIteration。 当调用 send() 来启动生成器时,它必须以 None 作为调用参数,因为这时没有可以接收值的 yield 表达式。

捕获协程异常的值

对于生成器,它可以及时捕获异常值,用于检视多次恢复中可能产生的异常。

看看下面的代码

def get_data(data):
    
    a = yield data 
            
    if a is None:   #此处的None为结尾,直接抛出异常,结束生成器

        print('第一次send:恢复执行')
        return '结束了'

    print('send的值:{}'.format(a))

data = get_data('挂起了')

print('第一次next:{}'.format(next(data)))  #必须调用next() 才能用send()

try:
    data.send(None) #结束生成器
except StopIteration as e:  
    print('抛出异常:{}'.format(e.value))

执行结果:
在这里插入图片描述
简单解释一下这个代码,第一行结果就是首次激活,在yield挂起,第二行由于send None,进入if语句分支,print了恢复执行

然后有一个return语句,不知道return到了哪里。然后就会抛出StopIteration异常,因为send没有得到下一个值,此时弹出进入except分支,通过一个.value属性拿到了return的值

关键就在这里了,为什么这个异常,有一个value属性呢?

官方文档是这样说的:

exception StopIteration
由内置函数 next() 和 iterator 的 next() 方法所引发,用来表示该迭代器不能产生下一项。

value 该异常对象只有一个属性 value,它在构造该异常时作为参数给出,默认值为 None。

当一个 generator 或 coroutine 函数返回时,将引发一个新的 StopIteration实例,函数返回的值将被用作异常构造器的 value 形参。

上述资料链接:
https://docs.python.org/zh-cn/3/library/exceptions.html#StopIteration

简单来说就是,由于你有一个return,表明当前生成器或协程是必须结束的状态,不可能会得到下一个next值,此时会产生一个新的StopIteration实例,并把返回值填充至它的value属性中。

yield from

官网链接
https://docs.python.org/zh-cn/3/whatsnew/3.3.html#pep-380

官方文档是这样说的:

允许生成器将其部分操作委托给另一个生成器。这允许包含的一段代码被分解出来并放置在另一个生成器中。此外,允许子生成器返回一个值,并且该值可供委托生成器使用。

但是,与普通循环不同,它允许子生成器直接从调用范围接收发送和抛出的值,并将最终值返回到外部生成器

简单来说就是,yield from 和 for循环很类似,但yield from功能更强大,yield from 还能自动捕获StopIteration异常,并输出异常对象的value属性。

看下面这段代码

def accumulate():
    tally = 0
    while 1:
        next = yield
        if next is None:
            return tally
        tally += next

def gather_tallies(tallies):
    while 1:
        tally = yield from accumulate()
        tallies.append(tally)

tallies = []
acc = gather_tallies(tallies)
next(acc)  # Ensure the accumulator is ready to accept values
for i in range(4):
    acc.send(i)

acc.send(None)  # Finish the first tally
for i in range(5):
    acc.send(i)

acc.send(None)  # Finish the second tally


tallies
[6, 10]

这是官方的代码,这里就可以看出来。一旦输入的值发生了异常,我们就可以return退出生成器,同时利用yield from进行捕获,不需要再写try-except,和呼出value属性。

协程新写法

了解了半协程,来看看真协程是怎么写的。协程是一个函数,只是它满足以下特点:

1. 有 IO 依赖的操作
2. 可以在进行 IO 操作时暂停
3. 无法直接执行

第一点就是输入输出流,就是前面提到的send方法,yield from。第二点就是可以进行挂起。第三点表示要使用next来进行迭代,不能直接执行。

它的作用就是对有大量 IO 操作的程序进行加速
Python 协程属于 可等待 对象
因此可以在其他协程中被等待
注意:

  1. 协程是单线程的,只是控制自己,而多线程是切换线程,切换线程需要不少资源
  2. 协程可以和多线程一起用,达到最高效率

在3.5版本之前是这样的:

@asyncio.coroutine
def old_data():#旧写法
    print('正在执行')
    yield from asyncio.sleep(2)#沉睡2秒                              
    print('执行完毕')

此时将asyncio作为装饰器使用,使用了yield from来接受错误信息

asyncio

接上面,现在新版的写法是这样

import asyncio

async def new_data():
    print('正在执行')
    await asyncio.sleep(2)#沉睡2秒
    print('执行完毕')

asyncio.run(new_data())#运行新写法

@asyncio.coroutine 替换为 async
yield from 替换为 await

文章来源:https://blog.csdn.net/bin789456/article/details/135112085
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。