Python 协程的基本概念

Python 协程的基本概念

在学习 Python 基础的过程中,遇到了比较难理解的地方,那就是协程。刚开始看了廖雪峰老师的博客,没怎么看懂,后面自己多方位 google 了一下,再回来看,终于看出了点眉目,在此总结下。

什么是 yield 和 yield from

yield

在学习协程之前,要先搞懂几个基本语法,那就是 yield 和 yield from,这也是陆续困扰我几天的问题,等这两个概念弄懂以后,后面的事情就比较简单了。

  • yield 是一个关键字,当一个方法中带有 yield 时,它就不是一个普通的方法了,而是变成了一个所谓的“生成器”。
  • 生成器不会一下子把所有值都返回给你,可以使用 next() 方法来调用,来不断取值。
  • 当生成器中执行到 yield 的时候,会从 yield 处返回结果,并保留上下文,等下一次 next() 的时候,会从上次的 yield 处继续执行。
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

f = fib(10)

print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))

上面是一个经典的斐波那契数列的生成函数,每次调用next都会从 yield 处返回结果。

输出:

1
1
2
3
5
Traceback (most recent call last):
  File "test.py", line 116, in 
    print(next(f))
StopIteration: done

遇到 return 会抛出异常,并将 return 的值包含在异常中抛出来。

一般我们不会一直调用 next() , 而是使用 for 循环:

for b in fib(5):
    print(b)

输出:

1
1
2
3
5

此处没有抛出异常,原因还待解释。。

使用 yield 一个一个地返回结果有什么作用?那是因为这样可以边循环边计算,边返回结果,不用创建完整的 list ,省去大量的内存空间。

接下来的关键点,可以通过 yield 传递参数!这个地方我也是弄了好久才弄明白的。。看下面的生产者和消费者的例子:

def customer():
    r = '404 empty'
    while True:
        print('star consume ..')
        n = yield r  # 2
        print('consuming {} ..'.format(n))
        r = '200 ok'


def producer(c):
    r = c.send(None)  # 1
    print(r)  # 3
    n = 0
    while n < 10:
        n += 1
        print('producing {} ..'.format(n))
        r = c.send(n)  # 4
        print('consumer {} return'.format(r))
    c.close()


if __name__ == '__main__':
    producer(customer())

输出:

star consume ..
404 empty
producing 1 ..
consuming 1 ..
star consume ..
consumer 200 ok return
producing 2 ..
consuming 2 ..
star consume ..
consumer 200 ok return
producing 3 ..
consuming 3 ..
star consume ..
consumer 200 ok return
producing 4 ..
consuming 4 ..
star consume ..
consumer 200 ok return
producing 5 ..
consuming 5 ..
star consume ..
consumer 200 ok return

先说明一下,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 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,使两者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程添加异常处理的代码。

这句话理解起来很麻烦,参考以下代码:

def A():
    yield from B()
    yield from C()

def B():
    yield '001'
    yield '002'
    yield '003'

def C():
    yield '004'
    yield '005'

if __name__ == '__main__':
    for s in A():
        print(s)

输出:

001
002
003
004
005

由此可见,生成器 A 通过 yield from 将任务下发给了生成器 B 和生成器 C 来执行了。yield from 可以很方便地拆分生成器,变为几个小生成器,方便代码管理。

什么是协程

根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。

换句话说就是你可以中断函数执行,转而执行别的函数。听起来就像是你正在烧水,在此期间你可以做下一个事情,而不用等水烧开。

以前我们都是用多线程和锁来做这个事情,但是时常会担心线程安全和死锁问题,多线程切换还会产生额外的开销。现在使用协程,在一个线程里面就能完成这些任务,不用担心线程问题,没有使用任何锁,大大提高了执行效率。

上面生产者和消费者的例子,使用了 yield 简单的实现了一个协程代码。

asyncio

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。

asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

asyncio库使我们方便地实现协程,我们可以把多个协程方法扔到asyncio的消息循环中,asyncio就自动地帮我们调用并协调这些协程方法。

@asyncio.coroutine 装饰器,可以帮助我们把一个生成器装饰为协程方法。

import threading
import asyncio

@asyncio.coroutine
def hello(i):
    print('Hello world! {} {}'.format(i, threading.currentThread()))
    yield from asyncio.sleep(3)
    print('Hello again! {} {}'.format(i, threading.currentThread()))

loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

输出

Hello world! 2 <_MainThread(MainThread, started 140736287097792)>
Hello world! 1 <_MainThread(MainThread, started 140736287097792)>
// 中间停3秒
Hello again! 2 <_MainThread(MainThread, started 140736287097792)>
Hello again! 1 <_MainThread(MainThread, started 140736287097792)>

可以看到在第一个任务执行时遇到 yield from ,这时候程序不会等着,而是马上开始下一个任务。当然先开始哪个任务是随机的。看打印出来的线程信息显示,两个任务是在同一个线程执行。

协程帮助我们在一个线程里面异步执行多个任务,里面的任务是并发进行的。

async/await

为了简化并更好地标识异步 IO,从Python 3.5 开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。

使用也非常简单,只要把原来的 @asyncio.coroutine 替换为 async ,把 yield from 替换为 await 。

修改上一节的代码:

import threading
import asyncio

async def hello(i):
    print('Hello world! {} {}'.format(i, threading.currentThread()))
    await asyncio.sleep(3)
    print('Hello again! {} {}'.format(i, threading.currentThread()))

loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

协程,有什么用?

asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。

aiohttp 应运而生,它是由 asyncio 实现的 HTTP 框架,帮助我们快速地搭建一个异步的 web 应用。

使用它要先安装:

pip install aiohttp

利用它我们用一小段代码搭建一个小应用:

  • 访问"http://127.0.0.1:8000/"根目录,首页返回 b'

    Index

    '

  • 访问"http://127.0.0.1:8000/hello/world",根据URL参数返回文本hello, world!。

import asyncio
from aiohttp import web


async def index(request):
    await asyncio.sleep(1)
    return web.Response(body=b'

Index

', content_type='text/html') async def hello(request): await asyncio.sleep(1) text = '

hello, {}!

'.format(request.match_info['name']) return web.Response(body=text.encode('utf-8'), content_type='text/html') async def init(loop): app = web.Application(loop=loop) app.router.add_route('GET', '/', index) app.router.add_route('GET', '/hello/{name}', hello) srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000) print('Server started at http://127.0.0.1:8000...') return srv loop = asyncio.get_event_loop() loop.run_until_complete(init(loop)) loop.run_forever()

总结

在这里,我们初步了解了下协程的基本概念。使用 yield 和 yield from ,并且利用 asyncio 库,可以组合成一个由协程组成的异步程序。async/await 帮助我们简化协程代码。利用协程可以写出很强大的 web 应用,进一步的深入我们后面再细细探究。

Python 协程的基本概念_第1张图片
谁说代码不快乐,扫码关注,听猿哥叨叨,做一个快乐的程序猿。

你可能感兴趣的:(Python 协程的基本概念)