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协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

posted on 2018-07-17 22:40  欢喜等大年  阅读(154)  评论(0编辑  收藏  举报

导航