• 带你简单了解python协程和异步


    带你简单了解python的协程和异步

    前言

    对于学习异步的出发点,是写爬虫。从简单爬虫到学会了使用多线程爬虫之后,在翻看别人的博客文章时偶尔会看到异步这一说法。而对于异步的了解实在困扰了我好久好久,看了N遍廖雪峰python3协程和异步的文章,一直都是一知半解,也学不会怎么使用异步来写爬虫。于是翻看了其他关于异步的文章,才慢慢了解python的异步机制并学会使用,但是没看到有特别全面的文章,所以在参考别人的文章基础上,加上了自己的理解,写了出来,也算是自己的一个小总结。

    一.认识生成器

    生成器的产生其实比较容易理解,例如当我们要创建了0到1000000这样一个很大的列表但同时我们只需要取出部分数据,这样的需要并不少见,而显然这种做法浪费了大量的内存空间。而生成器的作用就是为了解决上述的问题,利用生成器我们只需要能够保持一个整数的内存即可遍历数组列表。生成器的使用是通过yield实现,看下面代码样例。

    def l_range(num):
        index = 0
        while index < num:
            yield index    # (1)
            index += 1
    
    l = l_range(5)
    print(next(l))    #0
    print(next(l))    #1
    print(next(l))    #2
    

    很多人会混淆yield和send(后面会提到)的使用,上面的代码中 yield index,配合next(l)的使用。简单可以这样理解,函数l_range的while循环中,每次程序运行到(1)处都"暂停"了,向调用函数处返回index参数,注意此时并没有执行(1)这条语句!!!而每调用一次next(l)循环就会执行一次,而当index>num的时候,假若再调用next(l),因为此时已经跳出了while循环,yield不会再执行,所以会抛出异常。
    除了使用next()调用生成器,但是实际上还可以用for循环遍历,可知生成器也是可迭代对象。

    for i in l_range(5):
        print(i)
    

    明白了“暂停”的概念,生成器就变得非常好理解了!

    二.认识协程

    从上面的demo中,我们可以得知生成器的引入使得函数的调用能够“暂停”并且向外传递数据,既然可以向外传递数据,那么是否能够向函数里传递数据呢?生成器send的引入就是为了实现这个需求!send能够从生成器(函数)调用处传递数据到yield处。
    来看下面这个demo。

    def jumping_range(up_to):
        index = 0
        while index < up_to:
            jump = yield index    # (1)
            # print('index = %s, jump = %s' % (index, jump))
            if jump is None:
                jump = 1
            index += jump
    
    iterator = jumping_range(5)
    print(next(iterator))         #0
    print(iterator.send(2))        #2
    print(next(iterator))         #3
    print(iterator.send(-1))      #2
    print(next(iterator))         #3
    print(next(iterator))         #4
    

    下面解释下每一个输出,当第一次next(iterator),程序执行到(1)处,但是未执行,只是把index传递出去,所以此时输出的是0(index=0)。接着执行iterator.send(2),这里把2从调用处传递给了生成器里并赋值给jump,注意yield index是传递index参数出去,而jump=yield是把参数传递进去给jump!!!然后执行完while的第一次循环回到(1),此时index 执行了一次 index+=jump,并且jump=2。所以iterator.send(2)的输出是2!而后面的输出请各位独自推算一下,若实在想不通可以尝试在生成器中print一下各参数出来,方便理解。
    要搞明白协程,对于这句代码的理解尤为重要。

    jump = yield index
    

    其实意思上可以理解为

    jump = yield
    yield index
    

    即 jump接受从外面传递进来的参数,而index则是要传递出去的参数。但是当然,这只是我为了方便理解拆分出来的代码,实际上这样拆分会导致不同的结果。

    来看看拆分出来的代码

    def jumping_range(up_to):
        index = 0
        while index < up_to:
            jump = yield    #(a)
            yield index    #(b)
            # print('index = %s, jump = %s' % (index, jump))
            if jump is None:
                jump = 1
            # print('jump = %d' % jump)
            index += jump
    
    
    iterator = jumping_range(5)
    print(next(iterator))         #None
    print(iterator.send(2))        #0
    print(next(iterator))         #None
    print(iterator.send(-1))      #2
    print(next(iterator))         #None
    print(next(iterator))         #1
    

    简单讲解上述的输出,首先当程序执行到a(注意a处的代码未执行),此时yield 右边并没有参数,所以第一个print返回的是None。而当执行iterator.send(2),程序在a处把2传递给参数jump,然后往下执行,当遇到第二个yield,程序又“暂停”了,即一个while循环里暂停2次!而执行到b处(b处的代码未执行)把index传递到出去,所以此时print返回的是0(index=0)。接着来的可以如此类推了!

    只要明白了上述2个demo,相信对于协程已经有一定的理解了。最后再提一下yield from的使用。yield from的使用类似函数调用,作用是让重构变得简单,也让你能够将生成器串联起来,使返回值可以在调用栈中上下浮动,不需对编码进行过多改动。

    def bottom():
        return (yield 42)
    
    
    def middle():
        return (yield from bottom())
    
    
    def top():
        return (yield from middle())
    
    
    
    gen = top()
    value = next(gen)
    print(value)
    try:
        value = gen.send(value * 2)
    except StopIteration as exc:
        print(exc)
        value = exc.value
    print(value)
    

    三.认识异步

    对于异步IO,就是你发起一个IO操作,却不用等它结束,你可以继续做其他事情,当它结束时,你会得到通知。而要理解异步async/await,首先要理解什么是事件循环。
    事件循环,在维基百科的解释是“一种等待程序分配事件或消息的编程架构”。简单的说事件循环就是“当A发生时,执行B”。对python来说,用来提供事件循环的asyncio被加入标准库,asyncio 重点解决网络服务中的问题,事件循环在这里将来自套接字(socket)的 I/O 已经准备好读和/或写作为“当A发生时”(通过selectors模块)。和多线程和多进程一样,Asyncio是并发的一种方式。但由于GIL(全局解释器锁)的存在,python的多线程以及Asyncio不能带来真正的并行。而可交给asyncio执行的任务,就是上述的协程!一个协程可以放弃执行,把机会给其他协程(即yield from 或await)。

    1.定义协程

    定义协程有2种常用的方式,

    • 在定义函数的时候加上async作为前缀
    • 使用python装饰器。

    前者是python3.5的新方式,而后者是3.4的方式(3.5也可用)。

    async def do_some_work(x):
        print("Waiting " + str(x))
        await asyncio.sleep(x)
    
    @asyncio.coroutine
    def do_some_work2(x):
        print("Waiting " + str(x))
        yield from asyncio.sleep(x)
    

    这样一来do_some_work便是一个协程,准确来说是一个协程函数,并且可以用asyncio.iscoroutinefunction来验证

    print(asyncio.iscoroutinefunction(do_some_work)) # True
    

    在解释await之前,我们先来说明一下协程可以做什么事

    • 等待另一个协程
    • 产生一个结果给正在等它的协程
    • 引发一个异常给正在等它的协程

    demo中asyncio.sleep()也是一个协程,await asyncio.sleep(x),顾名思义就是等待,等待asyncio.sleep(x)执行完后返回do_some_work这个协程。

    2.运行协程

    协程函数的调用与普通函数不同,要让协程对象运行的话,常用的方式有2中

    • 在另一个已经运行的协程用‘await’等待它(或者yield from)
    • 通过 ‘ensure_future’ 函数计划它的执行

    简单来说,只有loop运行了,协程才可能运行。所以在运行协程之前,必须先拿到当前线程缺省的loop,然后把协程对象交给loop.run_until_complete,协程对象随后会在loop里得到运行。

    loop = asyncio.get_event_loop()
    loop.run_until_complete(do_some_work(3))
    

    run_until_complete 是一个阻塞(blocking)调用,知道调用运行结束,才返回。而它的参数是一个future,但是我们上面传进去的确实协程对象,之所以可以这样,是因为它内部做了检查,对于协程会通过ensure_future函数把协程对象包装(wrap)成了future。
    所以我们可以改为:

    loop.run_until_complete(asyncio.ensure_future(do_some_work(3))
    

    上面的demo这都是用ensure_future函数计划它的执行, 来看看使用第一种方法

    tasks = [
      asyncio.ensure_future(do_some_work(1)),
      asyncio.ensure_future(do_some_work(3))
    ]
    loop.run_until_complete(asyncio.wait(tasks))
    

    注意: asyncio.wait本身是一个协程

    3.回调

    有时候当协程运行结束的时候,我们希望得到通知,以便判断程序执行的情况以及下一步数据的处理。这一需求可以通过往future添加回调来实现。

    def done_callback(cor):
        """
        协程的回调函数
        :param cor:
        :return:
        """
        print('Done')
    
    cor = asyncio.ensure_future(do_some_work(3))
    cor.add_done_callback(done_callback)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(cor)
    

    4.多个协程

    在实际运行异步中,往往是有多个协程,同时在一个loop里运行。于是需要使用asyncio.gather函数把多个协程交给loop。

    loop.run_until_complete(asyncio.gather(do_some_work(1), do_some_work(3)))
    

    当然协程一多起来,一条语句写起来就不方便了,可以先把协程存在列表里。

    coros = [do_some_work(1), do_some_work(3)]
    loop.run_until_complete(asyncio.gather(*coros))
    

    由于这两个协程是并发运行的,所以等待时间并不是1+3=4,而是以耗时比较长的那个。
    上面也提到run_until_complete的参数是future,而gather起聚合的作用,把多个futures包装成一个future,因为loop.run_until_complete只接受单个future。上述代码也可以改为:

    coros = [asyncio.ensure_future(do_some_work(1)),
                 asyncio.ensure_future(do_some_work(3))]
    loop.run_until_complete(asyncio.gather(*coros))
    

    5.结束协程

    常用的结束协程的方法有2种:

    • run_until_complete
    • run_forever

    run_until_complete看函数名就大概明白,即是直到所有协程工作(future)结束才返回

    async def do_some_work(x):
        print('Waiting ' + str(x))
        await asyncio.sleep(x)
        print('Done')
    
    
    loop = asyncio.get_event_loop()
    
    coro = do_some_work(3)
    loop.run_until_complete(coro)
    

    输出:
    程序等待3秒钟后输出'Done'返回

    试试改为run_forever:

    async def do_some_work(x):
        print('Waiting ' + str(x))
        await asyncio.sleep(x)
        print('Done')
    
    
    loop = asyncio.get_event_loop()
    
    coro = do_some_work(3)
    asyncio.ensure_future(coro)
    
    loop.run_forever()
    

    输出:
    程序等待3秒钟后输出'Done'但并没有返回。
    run_forever会一直运行,直到loop.stop()被调用,但是不能在run_forever后调用stop,因为run_forever永远都不会返回,所以stop永远都不能被调用。

    loop.run_forever()
    loop.stop()
    

    正确的使用方法应该是在协程中调用stop,所以需要在协程参数中传入loop:

    async def do_some_work(loop, x):
        print('Waiting ' + str(x))
        await asyncio.sleep(x)
        print('Done')
        loop.stop()
    

    这样看来似乎没有什么问题,但是当有多个协程在loop里运行呢?

    asyncio.ensure_future(do_some_work(loop, 1))
    asyncio.ensure_future(do_some_work(loop, 3))
    
    loop.run_forever
    

    运行程序时会发现,只输出了一个‘Done’程序就返回了。这说明了第二个协程还没有结束,loop就停止了,被先结束的那个协程给停掉了。要解决这个问题,可以用gather把多个协程合并在一起,通过回调的方式调用loop.stop。

    async def do_some_work(loop, x):
        print('Waiting ' + str(x))
        await asyncio.sleep(x)
        print('Done')
    
    def done_callback(loop, futu):
        loop.stop()
    
    loop = asyncio.get_event_loop()
    
    futus = asyncio.gather(do_some_work(loop, 1), do_some_work(loop, 3))
    futus.add_done_callback(functools.partial(done_callback, loop))
    
    loop.run_forever()
    

    6. Close loop

    对于同一个loop,只要没有close,那么loop还可以继续添加协程并且再运行。

    loop.run_until_complete(do_some_work(loop, 1))
    loop.run_until_complete(do_some_work(loop, 3))
    

    但是关闭了就不能再运行了。

    loop.run_until_complete(do_some_work(loop, 1))
    loop.close()
    loop.run_until_complete(do_some_work(loop, 3))    # 抛出异常
    

    最后提一下yield from 和 await虽然内部机制有所不同,但是从作用来看基本上是一样的,这里就不探讨具体的区别了。
    另外关于asyncio.gather和asyncio.wait的区别请看StackOverflow的讨论Asyncio.gather vs asyncio.wait

    7.爬虫小demo

    使用asyncio异步抓取豆瓣电影top250

    # -*- coding: utf-8 -*-
    from lxml import etree
    from time import time
    import asyncio
    import aiohttp
    
    __author__ = 'lateink'
    
    url = 'https://movie.douban.com/top250'
    
    
    async def fetch_content(url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                return await response.text()
    
    
    async def parse(url):
        page = await fetch_content(url)
        html = etree.HTML(page)
    
        xpath_movie = '//*[@id="content"]/div/div[1]/ol/li'
        xpath_title = './/span[@class="title"]'
        xpath_pages = '//*[@id="content"]/div/div[1]/div[2]/a'
    
        pages = html.xpath(xpath_pages)
        fetch_list = []
        result = []
    
        for element_movie in html.xpath(xpath_movie):
            result.append(element_movie)
    
        for p in pages:
            fetch_list.append(url + p.get('href'))
    
        tasks = [fetch_content(url) for url in fetch_list]
        pages = await asyncio.gather(*tasks)
    
        for page in pages:
            html = etree.HTML(page)
            for element_movie in html.xpath(xpath_movie):
                result.append(element_movie)
    
        for i, movie in enumerate(result, 1):
            title = movie.find(xpath_title).text
            print(i, title)
    
    
    def main():
        loop = asyncio.get_event_loop()
        start = time()
        for i in range(5):
            loop.run_until_complete(parse(url))
        end = time()
        print('Cost {} seconds'.format((end - start)/5))
        loop.close()
    
    
    if __name__ == '__main__':
        main()
    
  • 相关阅读:
    黑白逆向编程课程笔记 8.静&动态地址&偏移
    黑白逆向编程课程笔记 7.CE使用(2)
    黑白逆向编程课程笔记 6.CE使用(1)
    传奇资源
    分布式——分布式发号器
    SpringBoot——属性注入
    SpringBoot——启动与自动配置类查找
    Mybatis——Spring事务实现
    SpringAOP——事务实现
    Linux——IO技术
  • 原文地址:https://www.cnblogs.com/lateink/p/7523011.html
Copyright © 2020-2023  润新知