# [译] Python 3.5 协程原理

·  阅读 14025

``````yield from iterator

（本质上）相当于：

``````for x in iterator:
yield x

## 关于 Python 协程的历史课

``````def eager_range(up_to):
"""Create a list of integers, from 0 to up_to, exclusive."""
sequence = []
index = 0
while index < up_to:
sequence.append(index)
index += 1
return sequence

``````def lazy_range(up_to):
"""Generator to return the sequence of integers from 0 to up_to, exclusive."""
index = 0
while index < up_to:
yield index
index += 1

``````def jumping_range(up_to):
"""Generator for the sequence of integers from 0 to up_to, exclusive.

Sending a value into the generator will shift the sequence by that amount.
"""
index = 0
while index < up_to:
jump = yield index
if jump is None:
jump = 1
index += jump

if __name__ == '__main__':
iterator = jumping_range(5)
print(next(iterator))  # 0
print(iterator.send(2))  # 2
print(next(iterator))  # 3
print(iterator.send(-1))  # 2
for x in iterator:
print(x)  # 3, 4

``````def lazy_range(up_to):
"""Generator to return the sequence of integers from 0 to up_to, exclusive."""
index = 0
def gratuitous_refactor():
while index < up_to:
yield index
index += 1
yield from gratuitous_refactor()

`yield from` 通过让重构变得简单，也让你能够将生成器串联起来，使返回值可以在调用栈中上下浮动，而不需对编码进行过多改动。

``````def bottom():
# Returning the yield lets the value that goes up the call stack to come right back
# down.
return (yield 42)

def middle():
return (yield from bottom())

def top():
return (yield from middle())

# Get the generator.
gen = top()
value = next(gen)
print(value)  # Prints '42'.
try:
value = gen.send(value * 2)
except StopIteration as exc:
value = exc.value
print(value)  # Prints '84'.

## 总结

Python 2.2 中的生成器让代码执行过程可以暂停。Python 2.5 中可以将值返回给暂停的生成器，这使得 Python 中协程的概念成为可能。加上 Python 3.3 中的 `yield from`，使得重构生成器与将它们串联起来都很简单。

## Python 3.4 中的方式

``````import asyncio

@asyncio.coroutine
def countdown(number, n):
while n > 0:
print('T-minus', n, '({})'.format(number))
yield from asyncio.sleep(1)
n -= 1

loop = asyncio.get_event_loop()
asyncio.ensure_future(countdown("A", 2)),
asyncio.ensure_future(countdown("B", 3))]
loop.close()

Python 3.4 中，`asyncio.coroutine` 修饰器用来标记作为协程的函数，这里的协程是和`asyncio`及其事件循环一起使用的。这赋予了 Python 第一个对于协程的明确定义：实现了PEP 342添加到生成器中的这一方法的对象，并通过[`collections.abc.Coroutine`这一抽象基类]表征的对象。这意味着突然之间所有实现了协程接口的生成器，即便它们并不是要以协程方式应用，都符合这一定义。为了修正这一点，`asyncio` 要求所有要用作协程的生成器必须`asyncio.coroutine`修饰

## Python 3.5 从 `yield from` 到 `await`

``````# This also works in Python 3.5.
@asyncio.coroutine
def py34_coro():
yield from stuff()

Python 3.5 添加了`types.coroutine` 修饰器，也可以像 `asyncio.coroutine` 一样将生成器标记为协程。你可以用 `async def` 来定义一个协程函数，虽然这个函数不能包含任何形式的 `yield` 语句；只有 `return``await` 可以从协程中返回值。

``````async def py35_coro():
await stuff()

`yield from``await` 在底层的差别是什么（也就是`types.coroutine``async def`的差别）？让我们看一下上面两则Python 3.5代码的例子所产生的字节码在本质上有何差异。`py34_coro()`的字节码是：

``````>>> dis.dis(py34_coro)
3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
6 GET_YIELD_FROM_ITER
10 YIELD_FROM
11 POP_TOP
15 RETURN_VALUE

`py35_coro()`的字节码是：

``````>>> dis.dis(py35_coro)
3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
6 GET_AWAITABLE
10 YIELD_FROM
11 POP_TOP
15 RETURN_VALUE

### 总结

awaitable 对象要么是一个协程要么是一个定义了`__await__()`方法的对象 – 也就是`collections.abc.Awaitable` – 且`__await__()`必须返回一个不是协程的迭代器。`await`表达式基本上与`yield from`相同但只能接受awaitable对象（普通迭代器不行）。`async`定义的函数要么包含`return`语句 – 包括所有Python函数缺省的`return None` – 和/或者 `await`表达式（`yield`表达式不行）。`async`函数的限制确保你不会将基于生成器的协程与普通的生成器混合使用，因为对这两种生成器的期望是非常不同的。

## 将 `async`/`await` 看做异步编程的 API

David 将 `async`/`await` 看作是异步编程的API创建了 `curio` 项目来实现他自己的事件循环。这帮助我弄清楚 `async`/`await` 是 Python 创建异步编程的原料，同时又不会将你束缚在特定的事件循环中也无需与底层的细节打交道（不像其他编程语言将事件循环直接整合到语言中）。这允许像 `curio` 一样的项目不仅可以在较低层面上拥有不同的操作方式（例如 `asyncio` 利用 future 对象作为与事件循环交流的 API，而 `curio` 用的是元组），同时也可以集中解决不同的问题，实现不同的性能特性（例如 `asyncio` 拥有一整套框架来实现运输层和协议层，从而使其变得可扩展，而 `curio` 只是简单地让用户来考虑这些但同时也让它运行地更快）。

## 一个例子

``````import datetime
import heapq
import types
import time

"""Represent how long a coroutine should before starting again.

Comparison operators are implemented for use by heapq. Two-item
tuples unfortunately don't work because when the datetime.datetime
instances are equal, comparison falls to the coroutine and they don't
implement comparison methods, triggering an exception.

"""

def __init__(self, wait_until, coro):
self.coro = coro
self.waiting_until = wait_until

def __eq__(self, other):
return self.waiting_until == other.waiting_until

def __lt__(self, other):
return self.waiting_until < other.waiting_until

class SleepingLoop:

"""An event loop focused on delaying execution of coroutines.

Think of this as being like asyncio.BaseEventLoop/curio.Kernel.
"""

def __init__(self, *coros):
self._new = coros
self._waiting = []

def run_until_complete(self):
# Start all the coroutines.
for coro in self._new:
wait_for = coro.send(None)
# Keep running until there is no more work to do.
while self._waiting:
now = datetime.datetime.now()
# Get the coroutine with the soonest resumption time.
# We're ahead of schedule; wait until it's time to resume.
time.sleep(delta.total_seconds())
now = datetime.datetime.now()
try:
# It's time to resume the coroutine.
except StopIteration:
# The coroutine is done.
pass

@types.coroutine
def sleep(seconds):
"""Pause a coroutine for the specified number of seconds.

Think of this as being like asyncio.sleep()/curio.sleep().
"""
now = datetime.datetime.now()
wait_until = now + datetime.timedelta(seconds=seconds)
# Make all coroutines on the call stack pause; the need to use `yield`
# necessitates this be generator-based and not an async-based coroutine.
actual = yield wait_until
# Resume the execution stack, sending back how long we actually waited.
return actual - now

async def countdown(label, length, *, delay=0):
"""Countdown a launch for `length` seconds, waiting `delay` seconds.

This is what a user would typically write.
"""
print(label, 'waiting', delay, 'seconds before starting countdown')
delta = await sleep(delay)
print(label, 'starting after waiting', delta)
while length:
print(label, 'T-minus', length)
waited = await sleep(1)
length -= 1
print(label, 'lift-off!')

def main():
"""Start the event loop, counting down 3 separate launches.

This is what a user would typically write.
"""
loop = SleepingLoop(countdown('A', 5), countdown('B', 3, delay=2),
countdown('C', 4, delay=1))
start = datetime.datetime.now()
loop.run_until_complete()
print('Total elapsed time is', datetime.datetime.now() - start)

if __name__ == '__main__':
main()