带你简单了解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()
带你简单了解python协程和异步的更多相关文章
- python协程与异步协程
在前面几个博客中我们一一对应解决了消费者消费的速度跟不上生产者,浪费我们大量的时间去等待的问题,在这里,针对业务逻辑比较耗时间的问题,我们还有除了多进程之外更优的解决方式,那就是协程和异步协程.在引入 ...
- Day10 - Python协程、异步IO、redis缓存、rabbitMQ队列
Python之路,Day9 - 异步IO\数据库\队列\缓存 本节内容 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 Python连接Mysql数据库操作 RabbitM ...
- Python协程、异步IO
本节内容 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 Python连接Mysql数据库操作 RabbitMQ队列 Redis\Memcached缓存 Paramiko SS ...
- python协程与异步I/O
协程 首先要明确,线程和进程都是系统帮咱们开辟的,不管是thread还是process他内部都是调用的系统的API,而对于协程来说它和系统毫无关系; 协程不同于线程的是,线程是抢占式的调度,而协程是协 ...
- 进击的Python【第十章】:Python的高级应用(多进程,进程间通信,协程与异步,牛逼的IO多路复用)
Python的socket高级应用(多进程,协程与异步) 一.多进程multiprocessing multiprocessing is a package that supports spawnin ...
- Python协程与Go协程的区别二
写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...
- Python核心技术与实战——十六|Python协程
我们在上一章将生成器的时候最后写了,在Python2中生成器还扮演了一个重要的角色——实现Python的协程.那什么是协程呢? 协程 协程是实现并发编程的一种方式.提到并发,肯很多人都会想到多线程/多 ...
- 异步等待的 Python 协程
现在 Python 已经支持用协程进行异步处理.但最近有建议称添加协程以全面完善 Python 的语言结构,而不是像现在这样把他们作为生成器的一个类型.此外,两个新的关键字---异步(async)和等 ...
- Python实现基于协程的异步爬虫
一.课程介绍 1. 课程来源 本课程核心部分来自<500 lines or less>项目,作者是来自 MongoDB 的工程师 A. Jesse Jiryu Davis 与 Python ...
随机推荐
- Hbase架构与原理
Hbase架构与原理 HBase是一个分布式的.面向列的开源数据库,该技术来源于 Fay Chang所撰写的Google论文"Bigtable:一个结构化数据的分布式存储系统".就 ...
- Eclipse创建Maven项目报错的解决
报错1:Could not resolve archetype org.apache.maven.archetypes:maven-archetype-quickstart 起因:删除一个用quick ...
- ChromeDriver,IEDriver,Firefox配置
ChromeDriver: 下载ChromeDriver.exe,放入某个文件夹,如C:\Program Files (x86)\Google\Chrome\Application,把此路径加入pat ...
- android学习笔记之GridView的使用
除了listview会使用适配器外,还有一种就是GridView,listview是单列多行的显示形式,适用于多项目的查看.而GridView是多行多列的显示形式,一般用在查看图片样式的activit ...
- WEB测试常见BUG
翻页 翻页时,没有加载数据为空,第二页数据没有请求 翻页时,重复请求第一页的数据 翻页时,没有图片的内容有时候会引用有图片的内容 2.图片数据为空 图片数据为空时,会保留为空的图片数据位置 ...
- SQL 创建存储过程,让主键自增
1. 首先创建存储过程: 2. 然后分别创建序列,生成基金公司编号.基金代码.活期账号.理财账号.基金账户.合同号.要求如下: 基金公司编号,字母K+5位数字. 基金代码,字母V+6位数字. 活期 ...
- Windows常用的一些DOS下的CMD命令整理
Windows常用的一些DOS下的CMD命令整理... -------------- 正斜杠--/////-----向右倒--网址正斜杠反斜杠--\\\\------向左倒--文件路径反斜杠 DOS命 ...
- 【javascript】异步编年史,从“纯回调”到Promise
异步和分块——程序的分块执行 一开始学习javascript的时候, 我对异步的概念一脸懵逼, 因为当时百度了很多文章,但很多各种文章不负责任的把笼统的描述混杂在一起,让我对这个 JS中的重要概念 ...
- MyEclipse 2014各种优化设置
各种优化大整合,陆续更新使用中的问题,也欢迎大家提问. 1.通用优化设置:字体,UTF-8编码格式设置: http://jingyan.baidu.com/article/b907e627d2c5da ...
- tensorflow安装调试总结(持续更新)
这段时间需要部署tensorflow到linux上,由于堡垒机不能连外网,所以pip.apt-get.wget.git统统不能用,然后就是各种调试了,下面整理了一些遇到的问题和解决方案,供大家参考(C ...