在学习 Python 基础的过程中,遇到了比较难理解的地方,那就是协程。刚开始看了廖雪峰老师的博客,没怎么看懂,后面自己多方位 google 了一下,再回来看,终于看出了点眉目,在此总结下。
什么是 yield 和 yield from
yield
在学习协程之前,要先搞懂几个基本语法,那就是 yield 和 yield from,这也是陆续困扰我几天的问题,等这两个概念弄懂以后,后面的事情就比较简单了。
yield 是一个关键字,当一个方法中带有 yield 时,它就不是一个普通的方法了,而是变成了一个所谓的“生成器”。
生成器不会一下子把所有值都返回给你,可以使用 next() 方法来调用,来不断取值。
当生成器中执行到 yield 的时候,会从 yield 处返回结果,并保留上下文,等下一次 next() 的时候,会从上次的 yield 处继续执行。
上面是一个经典的斐波那契数列的生成函数,每次调用next都会从 yield 处返回结果。
输出:
遇到 return 会抛出异常,并将 return 的值包含在异常中抛出来。
一般我们不会一直调用 next() , 而是使用 for 循环:
输出:
此处没有抛出异常,原因还待解释。。
使用 yield 一个一个地返回结果有什么作用?那是因为这样可以边循环边计算,边返回结果,不用创建完整的 list ,省去大量的内存空间。
接下来的关键点,可以通过 yield 传递参数!这个地方我也是弄了好久才弄明白的。。看下面的生产者和消费者的例子:
输出:
先说明一下,send 方法可以给 yield 发送参数。
程序刚开始执行到“#1”处,这里必须先调用 send(None) 一下。此处可是有讲究的,学名叫“预激”,作用是先启动一下生成器,让它先卡在 yield ,所以此时程序在“#2”处中断了,并返回 r ,随后“#3”处打印出 “404 empty” 。
接下来程序来到“#4”处,又调用了 send 方法,此时 send 参数为1,所以“#2”处被重新激活,将 n 赋值为 1,然后继续向下执行。
接着又循环来到 “#2” 处,yield 将 r 返回,此处中断,来到“#4”处继续执行。如此不断循环,直到满足条件退出循环。
像这样,producer生产完,告诉customer消费,消费完再通知producer生产,这些事情都发生在同一个线程里面,因此没有多线程的锁和资源争夺的问题。至此,协程的初步面貌就已经浮出水面。
小 tip: 调用 send(None) ,就相当于调用 next()。
yield from
Python3.3 版本的 PEP 380 中添加了 yield from 语法,PEP380 的标题是 “syntax for delegating to subgenerator”(把指责委托给子生成器的句法)。由此我们可以知道,yield from 是可以实现嵌套生成器的使用。
yield from x 表达式对x对象做的第一件事是,调用 iter(x),获取迭代器。所以要求x是可迭代对象。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,使两者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程添加异常处理的代码。
这句话理解起来很麻烦,参考以下代码:
输出:
由此可见,生成器 A 通过 yield from 将任务下发给了生成器 B 和生成器 C 来执行了。yield from 可以很方便地拆分生成器,变为几个小生成器,方便代码管理。
什么是协程
根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。
换句话说就是你可以中断函数执行,转而执行别的函数。听起来就像是你正在烧水,在此期间你可以做下一个事情,而不用等水烧开。
以前我们都是用多线程和锁来做这个事情,但是时常会担心线程安全和死锁问题,多线程切换还会产生额外的开销。现在使用协程,在一个线程里面就能完成这些任务,不用担心线程问题,没有使用任何锁,大大提高了执行效率。
上面生产者和消费者的例子,使用了 yield 简单的实现了一个协程代码。
asyncio
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
asyncio库使我们方便地实现协程,我们可以把多个协程方法扔到asyncio的消息循环中,asyncio就自动地帮我们调用并协调这些协程方法。
@asyncio.coroutine 装饰器,可以帮助我们把一个生成器装饰为协程方法。
输出
可以看到在第一个任务执行时遇到 yield from ,这时候程序不会等着,而是马上开始下一个任务。当然先开始哪个任务是随机的。看打印出来的线程信息显示,两个任务是在同一个线程执行。
协程帮助我们在一个线程里面异步执行多个任务,里面的任务是并发进行的。
async/await
为了简化并更好地标识异步 IO,从Python 3.5 开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
使用也非常简单,只要把原来的 @asyncio.coroutine 替换为 async ,把 yield from 替换为 await 。
修改上一节的代码:
协程,有什么用?
asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。
aiohttp 应运而生,它是由 asyncio 实现的 HTTP 框架,帮助我们快速地搭建一个异步的 web 应用。
使用它要先安装:
利用它我们用一小段代码搭建一个小应用:
访问"http://127.0.0.1:8000/"根目录,首页返回
访问"http://127.0.0.1:8000/hello/world",根据URL参数返回文本hello, world!。
总结
在这里,我们初步了解了下协程的基本概念。使用 yield 和 yield from ,并且利用 asyncio 库,可以组合成一个由协程组成的异步程序。async/await 帮助我们简化协程代码。利用协程可以写出很强大的 web 应用,进一步的深入我们后面再细细探究。
谁说代码不快乐,听猿哥叨叨,做一个快乐的程序猿。
领取专属 10元无门槛券
私享最新 技术干货