Python(3)---从迭代器到异步IO


目录

1. 迭代(iteration)与迭代器(iterator)
  1.1 构建简单迭代器
  1.2 调用next()
  1.3 迭代器状态图
2. 生成器(generator)
  2.1 创建简单生成器
  2.2 利用函数定义生成器
3. 协程
  3.1 概念理解
  3.2 实例
4. 异步IO
  4.1 概念理解
  4.2 实例


1 迭代(iteration)与迭代器(iterator)

迭代是重复反馈过程的活动,其目的通常是为了接近并到达所需的目标或结果。每一次对过程的重复被称为一次“迭代”,而每一次迭代得到的结果会被用来作为下一次迭代的初始值。(维基百科)

iterator是实现了iterator.__iter__()和iterator.__next__()方法的对象iterator.__iter__()方法返回的是iterator对象本身。

1.1 构建简单迭代器

In [146]: test_iter = iter([i for i in range(1,4)]) 

In [147]: test_iter
Out[147]: <list_iterator at 0x84002a1f60>

返回列表迭代器对象,实际上实现了iterator.__iter__()。

1.2 调用next()

In [148]: next(test_iter)
Out[148]: 1

In [149]: next(test_iter)
Out[149]: 2

In [150]: next(test_iter)
Out[150]: 3

In [151]: next(test_iter)
Traceback (most recent call last):

  File "<ipython-input-151-ca50863582b2>", line 1, in <module>
    next(test_iter)

StopIteration
In [152]: 

可以看出next()实际调用了iterator.__next__()方法,每次调用更新iterator状态,令其指向后一项,以便下一次调用并返回当前结果。

1.3 迭代器状态图

图片来自网络

  实际上迭代器就是实现迭代功能,先初始化迭代器,利用next()方法实现重复调用更新值,上次的终值时本次的初值。

2 生成器(generator)

通常带有yield的函数便称为生成器,yield是生成器执行的暂停恢复点,也是实现generator的__next__()方法的关键!可以对yield表达式进行赋值,也可以将yield表达式的值返回。简而言之,generator是以更优雅的方式实现的iterator。

2.1 创建简单生成器

其创建方法区别于列表创建方式,在此采用()而非[]

In [163]: test_gene = (x * x for x in range(1,4))

In [164]: test_gene
Out[164]: <generator object <genexpr> at 0x00000084002AD8E0>

In [166]: test_gene.__next__()
Out[166]: 1

In [167]: test_gene.__next__()
Out[167]: 4

In [168]: test_gene.__next__()
Out[168]: 9

In [169]: test_gene.__next__()
Traceback (most recent call last):

  File "<ipython-input-169-e6166353d257>", line 1, in <module>
    test_gene.__next__()

StopIteration

2.2 利用函数定义生成器

In [173]: def test_gene(a):
     ...:     print("第一步")
     ...:     yield a
     ...:     a += 1
     ...:     print("第二步")
     ...:     yield a
     ...:     a += 1
     ...:     print("第三步")
     ...:     yield a
     ...:     a += 1
     ...: 
     ...: 
     ...: g = test_gene(1)  

In [174]: g
Out[174]: <generator object test_gene at 0x0000008400295620>

In [175]: g.__next__()
第一步
Out[175]: 1

In [176]: g.__next__()
第二步
Out[176]: 2

In [177]: g.__next__()
第三步
Out[177]: 3

In [178]: g.__next__()
Traceback (most recent call last):

  File "<ipython-input-178-60e4a84be5d7>", line 1, in <module>
    g.__next__()

StopIteration

可以看出如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。在每次调用next()的时候执行:

  • 遇到yield语句返回;
  • 保留上下文环境(保留局部变量状态);
  • 再次执行时从上次返回的yield语句处继续执行。

总的来说生成器是一类特殊迭代器,一个产生值的函数 yield 是一种产生一个迭代器却不需要构建迭代器的精密小巧的方法。很明显可以看出生成器(Generator)是采用边循环边计算的机制,当我们只需访问一个大列表的前几个元素的情况下可以不必创建完整的list,从而节省大量的空间。

3 协程

3.1 概念理解

线程与进程,有自己的上下文,调度是由CPU来决定调度的;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制(程序员控制),其实就是在一个线程中切换子线程。
  相比多线程有如下好处:一是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,当线程数量越多,协程的性能优势就越明显。二是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
  协程、线程、进程在不同场景下的适用性不尽相同,在其他语言中,协程的其实是意义不大的多线程即可已解决I/O的问题,但是在python因为有GIL(Global Interpreter Lock 全局解释器锁 )在同一时间只有一个线程在工作,所以如果一个线程里面I/O操作特别多,协程就比较适用,如网络请求。

3.2 实例

Python中的协程是通过“生成器(generator)”的概念实现的。这里引用廖雪峰Python教程中的例子,并将其修改为定外卖场景:

def shop():
    '''定义商家(生成器)
    '''        
    print("[-商家-] 开始接单 ......")
    print("###############################")
    r = "商家第1次接单完成"       # 初始化返回结果,并在启动商家时,返回给消费者
    while True:    
        n = yield r  # (n = yield):商家通过yield接收消费者的消息,(yield r):返给结果  
        print("[-商家-] 正在处理第%s次订单 ......" % n)
        print("[-商家-] 第%s次订单正在配送中 ......" % n)
        print("[-商家-] 第%s次订单已送达" % n)
        r = "商家第%s次接单完成" % (n+1)     # 商家信息,下个循环返回给消费者

def consumer(g):  
    '''定义消费者
    @g:商家生成器
    '''       
    print("[消费者] 开始下单 ......")
    r = g.send(None)    # 启动商家生成器  
    n = 0
    while n < 5:
        n += 1
        print("[消费者] 已下第%s单" % n)
        print("[消费者] 接受商家消息:%s" % r)
        r = g.send(n)   # 向商家发送下单消息并准备接收结果。此时会切换到消费者执行
        print("###############################")
    g.close()           # 关闭商家生成器
    print("[消费者] 停止接单 ......")

if __name__ == "__main__":
    g = shop() 
    consumer(g)
[消费者] 开始下单 ......
[-商家-] 开始接单 ......
###############################
[消费者] 已下第1单
[消费者] 接受商家消息:商家第1次接单完成
[-商家-] 正在处理第1次订单 ......
[-商家-] 第1次订单正在配送中 ......
[-商家-] 第1次订单已送达
###############################
[消费者] 已下第2单
[消费者] 接受商家消息:商家第2次接单完成
[-商家-] 正在处理第2次订单 ......
[-商家-] 第2次订单正在配送中 ......
[-商家-] 第2次订单已送达
###############################
[消费者] 已下第3单
[消费者] 接受商家消息:商家第3次接单完成
[-商家-] 正在处理第3次订单 ......
[-商家-] 第3次订单正在配送中 ......
[-商家-] 第3次订单已送达
###############################
[消费者] 已下第4单
[消费者] 接受商家消息:商家第4次接单完成
[-商家-] 正在处理第4次订单 ......
[-商家-] 第4次订单正在配送中 ......
[-商家-] 第4次订单已送达
###############################
[消费者] 已下第5单
[消费者] 接受商家消息:商家第5次接单完成
[-商家-] 正在处理第5次订单 ......
[-商家-] 第5次订单正在配送中 ......
[-商家-] 第5次订单已送达
###############################
[消费者] 停止接单 ......

4 异步IO实例

4.1 概念理解

异步是区别于同步,这里的同步指的并不是所有线程同时进行,而是所有线程在时间轴上有序进行。在实际的IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。异步正是为解决CPU高速执行能力和IO设备的龟速严重不匹配,当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
  异步IO是基于CPU与IO处理速度不一致并为了充分利用资源的方法之一,在上一篇《Python知识(1)——并发编程》中记录到的多线程与多进程也是该问题的处理方法之一。

图片来自网络

4.2 实例

只有协程还不够,还不足以实现异步IO,我们必须实现消息循环和状态的控制,在此我们先了解一下几个关键词。

  • asyncio
    Python 3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

  • async/await
    python3.5中新加入的特性, 将异步从原来的yield 写法中解放出来,变得更加直观。其中async修饰的函数为异步函数,await 替换了yield from, 表示这一步为异步操作。

  • aiohttp
    一个提供异步web服务的库,分为服务器端和客户端。这里主要使用其客户端。

import asyncio
import aiohttp
async def get(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            print(url, resp.status)
            print(url, await resp.text())

loop = asyncio.get_event_loop()     # 得到一个事件循环模型
urls = ["https://movie.douban.com/tag/科幻?start="+str(1)+"&type=T" for i in range(1,4)]
tasks = [ get(url) for url in urls] # 初始化任务列表

loop.run_until_complete(asyncio.wait(tasks))    # 执行任务
loop.close()                        # 关闭事件循环列表

参考与拓展阅读:
[1]Python生成器详解 | 投稿
[2]廖雪峰Python教程
[3]Python学习:异步IO:协程和asyncio
[4]Python【第十篇】协程、异步IO
[5]Python进阶:理解Python中的异步IO和协程(Coroutine),并应用在爬虫中
[6]异步爬虫: async/await 与 aiohttp的使用,以及例子
[7]Python 异步网络爬虫(1)


个人Github
个人博客DebugNLP
欢迎各路同学互相交流

推荐阅读更多精彩内容