廖慧敏
本文参考了:
yield 和 yield from
先让我们来学习或者回顾一下yield
和yield from
的用法。如果你很自信自己完成理解了,可以跳到下一部分。
Python3.3提出了一种新的语法:yield from
。
yield from iterator
复制代码
本质上也就相当于:
for x in iterator:
yield x
复制代码
下面的这个例子中,两个 yield from
加起来,就组合得到了一个大的iterable
(例子来源于官网3.3 release):
>>> def g(x):
... yield from range(x, 0, -1)
... yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
复制代码
理解 yield from
对于接下来的部分至关重要。想要完全理解 yield from
,还是来看看官方给的例子:
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
print(tallies)
复制代码
我还专门为此录制了一段视频,你可以配合文字一起看,或者你也可以打开 pycharm 以及任何调试工具,自己调试一下。 视频链接
来一起 break down:
从acc = gather_tallies(tallies)
这一行开始,由于gather_tallies
函数中有一个 yield,所以不会while 1
立即执行(你从视频中可以看到,acc 是一个 generator 类型)。
next(acc)
:
next()会运行到下一个 yield,或者报StopIteration错误。
next(acc)
进入到函数体gather_tallies,gather_tallies中有一个yield from accumulate()
,next(acc)不会在这一处停,而是进入到『subgenerator』accumulate里面,然后在next = yield
处,遇到了yield
,然后暂停函数,返回。
for i in range(4):
acc.send(i)
复制代码
理解一下 acc.send(value)
有什么用:
- 第一步:回到上一次暂停的地方
- 第二步:把value 的值赋给
xxx = yield
中的xxx
,这个例子中就是next
。
accumulate
函数中的那个while 循环,通过判断next
的值是不是 None 来决定要不要退出循环。在for i in range(4)
这个for循环里面,i 都不为 None,所以 while 循环没有断。但是,根据我们前面讲的:next()会运行到下一个 yield的地方停下来,这个 while 循环一圈,又再次遇到了yield
,所以他会暂停这个函数,把控制权交还给主线程。
理清一下:对于accumulate来说,他的死循环是没有结束的,下一次通过 next()恢复他运行时,他还是在运行他的死循环。对于gather_tallies来说,他的
yield from accumulate()
也还没运行完。对于整个程序来说,确实在主进程和accumulate函数体之间进行了多次跳转。
接下来看第一个acc.send(None)
:这时next
变量的值变成了None
,if next is None
条件成立,然后返回tally
给上一层函数。(计算一下,tally 的值为0 + 1 + 2 + 3 = 6)。这个返回值就赋值给了gather_tallies
中的gally
。这里需要注意的是,gather_tallies
的死循环还没结束,所以此时调用next(acc)
不会报StopIteration
错误。
for i in range(5):
acc.send(i)
acc.send(None) # Finish the second tally
复制代码
这一部分和前面的逻辑是一样的。acc.send(i)会先进入gather_tallies
,然后进入accumulate
,把值赋给next
。acc.send(None)
停止循环。最后tally的值为10(0 + 1 + 2 + 3 + 4)。
最终tallies列表为:[6,10]
。
Python async await发展简史
看一下 wikipedia 上 Coroutine的定义:
Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.
关键点在于by allowing execution to be suspended and resumed.(让执行可以被暂停和被恢复)。通俗点说,就是:
coroutines are functions whose execution you can pause。(来自How the heck does async/await work in Python 3.5?)
这不就是生成器吗?
python2.2 - 生成器起源
Python生成器的概念最早起源于 python2.2(2001年)时剔除的 pep255,受Icon 编程语言启发。
生成器有一个好处,不浪费空间,看下面这个例子:
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
复制代码
如果用这个函数生成一个10W 长度的列表,需要等待 while 循环运行结束返回。然后这个sequence
列表将会占据10W 个元素的空间。耗时不说(从能够第一次能够使用到 sequence 列表的时间这个角度来看),占用空间还很大。
借助上一部分讲的yield
,稍作修改:
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
复制代码
这样就只需要占据一个元素的空间了,而且立即就可以用到 range,不需要等他全部生成完。
python2.5 : send stuff back
一些有先见之明的前辈想到,如果我们能够利用生成器能够暂停的这一特性,然后想办法添加 send stuff back 的功能,这不就符合维基百科对于协程的定义了么?
于是就有了pep342。
pep342中提到了一个send()
方法,允许我们把一个"stuff"送回生成器里面,让他接着运行。来看下面这个例子:
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
复制代码
这里的send
把一个『stuff』送进去给生成器,赋值给 jump,然后判断jump 是不是 None,来执行对应的逻辑。
python3.3 yield from
自从Python2.5之后,关于生成器就没做什么大的改进了,直到 Python3.3时提出的pep380。这个 pep 提案提出了yield from
这个可以理解为语法糖的东西,使得编写生成器更加简洁:
def lazy_range(up_to):
"""Generator to return the sequence of integers from 0 to up_to, exclusive."""
index = 0
def gratuitous_refactor():
nonlocal index
while index < up_to:
yield index
index += 1
yield from gratuitous_refactor()
复制代码
第一节我们已经详细讲过 yield from 了,这里就不赘述了。
python3.4 asyncio模块
插播:事件循环(eventloop)
如果你有 js 编程经验,肯定对事件循环有所了解。
理解一个概念,最好也是最有bigger的就是翻出 wikipedia:
an event loop "is a programming construct that waits for and dispatches events or messages in a program" - 来源于Event loop - wikipedia
简单来说,eventloop 实现当 A 事件发生时,做 B 操作。拿浏览器中的javaScript事件循环来说,你点击了某个东西(A 事件发生了),就会触发定义好了的onclick
函数(做 B 操作)。
在 Python 中,asyncio 提供了一个 eventloop(回顾一下上一篇的例子),asyncio 主要聚焦的是网络请求领域,这里的『A 事件发生』主要就是 socket 可以写、 socket可以读(通过selectors
模块)。
到这个时期,Python 已经通过Concurrent programming
的形式具备了异步编程的实力了。
Concurrent programming
只在一个 thread 里面执行。go 语言blog 中有一个非常不错的视频:Concurrency is not parallelism,很值得一看。
这个时代的 asyncio 代码
这个时期的asyncio代码是这样的:
import asyncio
# Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html.
@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()
tasks = [
asyncio.ensure_future(countdown("A", 2)),
asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
复制代码
输出结果为:
T-minus 2 (A)
T-minus 3 (B)
T-minus 1 (A)
T-minus 2 (B)
T-minus 1 (B)
复制代码
这时使用的是asyncio.coroutine
修饰器,用来标记某个函数可以被 asyncio 的事件循环使用。
看到yield from asyncio.sleep(1)
了吗?通过对一个asyncio.Future object yield from
,就把这个future object 交给了事件循环,当这个 object 在等待某件事情发生时(这个例子中就是等待 asyncio.sleep(1),等待 1s 过后),把函数暂停,开始做其他的事情。当这个future object 等待的事情发生时,事件循环就会注意到,然后通过调用send()
方法,让它从上次暂停的地方恢复运行。
break down 一下上面这个代码:
事件循环开启了两个countdown()
协程调用,一直运行到yield from asyncio.sleep(1)
,这会返回一个 future object,然后暂停,接下来事件循环会一直监视这两个future object。1秒过后,事件循环就会把 future object send()给coroutine,coroutine又会接着运行,打印出T-minus 2 (A)
等。
python3.5 async await
python3.4的
@asyncio.coroutine
def py34_coro():
yield from stuff()
复制代码
到了 Python3.5,可以用一种更加简洁的语法表示:
async def py35_coro():
await stuff()
复制代码
这种变化,从语法上面来讲并没什么特别大的区别。真正重要的是,是协程在 Python 中哲学地位的提高。 在 python3.4及之前,异步函数更多就是一种很普通的标记(修饰器),在此之后,协程变成了一种基本的抽象基础类型(abstract base class):class collections.abc.Coroutine。
How the heck does async/await work in Python 3.5?一文中还讲到了async
、await
底层 bytecode 的实现,这里就不深入了,毕竟篇幅有限。
把 async、await看作是API 而不是 implementation
Python 核心开发者(也是我最喜欢的 pycon talker 之一)David M. Beazley在PyCon Brasil 2015的这一个演讲中提到:我们应该把 async
和await
看作是API,而不是实现。 也就是说,async
、await
不等于asyncio
,asyncio
只不过是async
、await
的一种实现。(当然是asyncio
使得异步编程在 Python3.4中成为可能,从而推动了async
、await
的出现)
他还开源了一个项目github.com/dabeaz/curi…,底层的事件循环机制和 asyncio
不一样,asyncio
使用的是future object
,curio
使用的是tuple
。同时,这两个 library 有不同的专注点,asyncio
是一整套的框架,curio
则相对更加轻量级,用户自己需要考虑到事情更多。
How the heck does async/await work in Python 3.5?此文还有一个简单的事件循环实现例子,有兴趣可以看一下,后面有时间的话也许会一起实现一下。
总结一下
- 协程只有一个 thread。
- 操作系统调度进程、协程用事件循环调度函数。
- async、await 把协程在 Python 中的哲学地位提高了一个档次。
最重要的一点感受是:Nothing is Magic。现在你应该能够对 Python 的协程有了在整体上有了一个把握。
如果你像我一样真正热爱计算机科学,喜欢研究底层逻辑,欢迎关注我的微信公众号:
文章最后发布于: 2019-04-29 00:47:45
相关阅读
UDP编程流程: 服务器端:socket(), bind(), recvfrom()/sendto(), close(); 客户端:socket(), sendto()/recvfrom(), clo
与或非——编程语言中的!|| && 与离散数学中的!v ∧ 优
1.问题背景:一同事让看一段逻辑有没有问题,其中一个if如下: if(A || B && C){ ... } 是的,没有括号,平时根据需要会写成 if((A
最难学的10大编程语言排行榜,Java只排第三,第一名出乎意
2018年12月的TIOBE编程语言排行榜已经出炉,Python重回前三,Go语言跌出前十,Visual Basic.NET涨幅明显,保持第五名。TIOBE排行榜是根据
今年9月,深圳点猫科技有限公司(以下简称为编程猫)与北京故宫宫廷文化发展有限公司(以下简称为故宫宫廷文化)在故宫文化推广方面展
关于QtGraphics编程的几点经验总结_qgraphicsscene
转载自:http://www.niubb.com/riji/2015/10-01/12524.htmlQtGraphics模块用于显示二维的图形图像,所以三维的事情就不要找它了,应该