python入门20180717-迭代器、生成器和协程
迭代器、生成器和协程
python中任意的对象,只要它定义了可以返回一个迭代器的__iter__方法,或者支持下标索引的_getitem_方法,那么它就是一个可迭代对象。
可迭代的对象不一定就是迭代器;
比如:一个列表L=[1,2,3]是一个可迭代对象,但不是迭代器,l=iter(L)返回的是迭代器;
In [17]: L = [1,2,3,4] In [18]: l = iter(L) In [19]: next(l) Out[19]: 1 In [20]: next(l) Out[20]: 2 In [21]: l.__next__() Out[21]: 3 In [22]: l.__next__() Out[22]: 4 In [23]: l.__next__() --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-23-731686253790> in <module>() ----> 1 l.__next__() StopIteration:
什么是迭代器呢?
实现了__iter__和next的方法的对象就是迭代器,其中,__iter__方法但会迭代器对象本身,next方法返回容器的下一个元素,在没有后续元素时抛出StopIteration异常;
在python2中要定义的next方法名字不同,应该是__next__
为什么有了可迭代对象,还要有迭代器?
因为可迭代对象,是全部获取,占用内存;也是因为使用迭代器更通用,更简单、优雅;
迭代器可以用list函数转换为一个列表;
In [5]: class Fib: ...: def __init__(self,max): ...: self.a = 0 ...: self.b = 1 ...: self.max = max ...: ...: def __iter__(self): ...: return self ...: ...: def __next__(self): ...: fib = self.a ...: if fib > self.max: ...: raise StopIteration ...: self.a,self.b = self.b,self.a + self.b ...: return fib ...: In [6]: f = Fib(100) In [7]: for i in f: ...: print(i) ...: 0 1 1 2 3 5 8 13 21 34 55 89
In [45]: f.next()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-45-297e922ec2d6> in <module>()
----> 1 f.next()
AttributeError: 'Fib' object has no attribute 'next'
In [46]: next(f)
Out[46]: 0
In [47]: next(f)
Out[47]: 1
In [48]: next(f)
Out[48]: 1
In [51]: f.__next__()
Out[51]: 2
In [52]: f.__next__()
Out[52]: 3
In [8]: list(Fib(100)) #迭代器可以用list函数转换为一个列表; Out[8]: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
生成器
生成器是一种使用普通函数语法定义的迭代器。生成器和普通函数的区别是使用yield,而不是return返回值。
yield 中文有生产、产生的意思;
yield每次返回一个结果,在每个结果的中间会挂起函数;挂起函数的状态便于下次从离开的地方继续;生成器本质上也是迭代器;
对于 Python 生成器中的 yield 来说,yield item 这行代码会产出一个值,提供给 next(...) 的调用方;此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next();调用方会从生成器中拉取值。
生成器表达式
In [24]: g = (i for i in range(10) if i%2) #生成器表达式 In [25]: g Out[25]: <generator object <genexpr> at 0x7f2db837a620> In [26]: for i in g: ...: print(i) ...: 1 3 5 7 9
In [37]: g = (i for i in range(10) if i%2)
In [38]: g.next()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-38-7dbbdfed0980> in <module>()
----> 1 g.next()
AttributeError: 'generator' object has no attribute 'next'
In [39]: g
Out[39]: <generator object <genexpr> at 0x7f2dbb968570>
In [40]: next(g)
Out[40]: 1
In [41]: next(g)
Out[41]: 3
In [42]: next(g)
Out[42]: 5
从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数。可是,在协程中,yield 通常出现在表达式的右边(例如,datum = yield),可以产出值,也可以不产出——如果 yield关键字后面没有表达式,那么生成器产出 None。协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是 next(...) 函数。通常,调用方会把值推送给协程。
yield 关键字甚至还可以不接收或传出数据。不管数据如何流动,yield 都是一种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激活其他的协程。
从根本上把 yield 视作控制流程的方式 ,这样就好理解协程了。
来看个例子:
n [27]: def coroutine2(a): ...: print(f'Start: {a}') ...: b = yield a ...: print(f'Received: b = {b}') ...: c = yield a + b ...: print(f'Received: c = {c}') ...: In [28]: coro = coroutine2(1) In [29]: next(coro) #最先调用 next(my_coro) 函数 这一步通常称为 “预激”(prime)协程 (即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用) Start: 1 Out[29]: 1 In [30]: coro.send(2) Received: b = 2 Out[30]: 3 In [31]: coro.send(10) Received: c = 10 --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-31-7a1f101c1ec1> in <module>() ----> 1 coro.send(10) StopIteration:
协程可以身处四个状态中的一个:
当前状态可以使用inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个。
GEN_CREATED | 等待开始执行 |
GEN_RUNNING | 解释器正在执行 |
GEN_SUSPENDED | 在 yield 表达式处暂停 |
GEN_CLOSED | 执行结束 |
因为 send 方法 的参数 会成为暂停的 yield 表达式的值 ,所以,仅当协程处于暂停状态时才能调用 send 方法,例如 my_coro.send(10)。不过,如果协程还没激活(即,状态是 'GEN_CREATED'),情况就不同了。因此,始终要调用 next(my_coro) 激活协程——也可以调用my_coro.send(None),效果一样。
最先调用 next(my_coro) 函数 这一步通常称为 “预激”(prime)协程 (即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)。
如果创建协程对象后立即把 None 之外的值发给它,会出现下述错误:
In [32]: coro2 = coroutine2(1) In [33]: coro2.send(121) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-33-25e001dbff4a> in <module>() ----> 1 coro2.send(121) TypeError: can't send non-None value to a just-started generator In [34]: coro2.send(None) Start: 1 Out[34]: 1
关键的一点是, 协程在 yield 关键字所在的位置暂停执行 。前面说过, 在赋值语句中, = 右边的代码在赋值之前执行 。因此,对于 b =yield a 这行代码来说,等到客户端代码再激活协程时才会设定 b 的值。这种行为要花点时间才能习惯,不过一定要理解,这样才能弄懂异步编程中 yield 的作用。
simple_coro2 协程的执行过程分为 3 个阶段,如图下图所示。
(1) 调用 next(my_coro2),打印第一个消息,然后执行 yield a,产 出数字 14。
(2) 调用 my_coro2.send(28),把 28 赋值给 b,打印第二个消息,然 后执行 yield a + b,产出数字 42。
(3) 调用 my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协 程终止。
注意,各个阶段都在yield 表达式中结束,而且下一个阶段都从那一行代码开始,然后再把 yield 表达式的值赋给变量。
协程的特点及优势
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。
来看例子:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
import time def consumer(): r = '' while True: n = yield r print('[CONSUMER] Consuming %s...' % n) r = '200 OK' def produce(c): next(c) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() if __name__ == "__main__": c = consumer() produce(c) 输出: [PRODUCER] Producing 1... [CONSUMER] Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 2... [CONSUMER] Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 3... [CONSUMER] Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 4... [CONSUMER] Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] Producing 5... [CONSUMER] Consuming 5... [PRODUCER] Consumer return: 200 OK
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
【推荐】博客园携手 AI 驱动开发工具商 Chat2DB 推出联合终身会员
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步