无论你们认不认识Tornado,仍是简要地介绍一下吧。
Tornado是Facebook的一个开源的高性能Web服务器,最大的优点在于它基于事件的设计,使得它在IO密集型(好比代理或爬虫)应用上有着基于线程web框架没法比拟的巨大优点。 html
除此以外Tornado有一个很是赞的设计(固然Twisted等别的事件框架也有),基于Generator的协程模型。具体可参见这里,官方的example浅显易懂,一眼就能够看出协程比回调(包括闭包)的优点。 python
几乎全部的CryptoCoin相关网站都会提供很方便的Web API,另外也有一些第三方的API接口供使用。
有趣的是,这些API几乎所有都是Json格式的(究其缘由主要是Bitcoin客户端就使用的Json表示,因而programmer更倾向于这种方式),因此使用起来很方便。 web
下面咱们以btc-e的ticker API为例讲述一下代码演进的过程,对比一下几种实现方式的区别。 json
1
2
3
4
5
6
|
defget_btce():
globalcurrent
res=tornado.httpclient.HTTPClient().fetch(
"https://btc-e.com/api/2/ltc_btc/ticker"
)
current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']
|
最简单的同步写法。注意这种写法是不能够实际使用的,由于同步请求会阻塞当前线程,而通常来讲全部的事件处理在同一个线程,因此会致使整个服务器的全部其他动做(包括客户端请求的处理)都要在这个请求返回以后才能够被处理。 api
1
2
3
4
5
6
7
8
9
|
defget_btce():
defcallback(res):
globalcurrent
current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']
tornado.httpclient.AsyncHTTPClient().fetch(
"https://btc-e.com/api/2/ltc_btc/ticker",
callback=callback,
)
|
朴素的callback就是这样使用的,因为Python没有闭包因此写起来异常恶心(好吧),接下来咱们继续演进它。 服务器
上面提到的Tornado有利用Python Generator实现的协程,相应的模块叫作tornado.gen,从3.0开始gen能够吃Future了因此致使写起来比以前更简洁。 闭包
1
2
3
4
5
6
7
|
@tornado.gen.coroutine
defget_btce():
globalcurrent
res=yieldtornado.httpclient.AsyncHTTPClient().fetch(
"https://btc-e.com/api/2/ltc_btc/ticker"
)
current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']
|
会发现这个已经和同步几乎同样了,除了添加了一个修饰器gen.coroutine表示协程,以及在AsyncHTTPClient.fetch()的返回值上用了yield而已,是否是很方便w
讲得更细节一点:
首先使用gen.coroutine来表示协程。coroutine会生成一个wrapper,在外面层吃yield point,在原函数yield的时候会传给外层的wrapper处理,执行完以后再send()回来;
而后ASyncHTTPClient()返回一个Future(也向后兼容以前的callback方式)供回调,须要的童鞋能够本身看Future的写法;
当Future执行完成后,coroutine的wrapper会把结果(res)用generator的send()方法传递回用户代码,赋值给res变量,用户代码继续执行。 app
咱们须要在执行完一次请求以后等待1分钟再次请求,在线程写法中time.sleep()能够很好地解决这个问题,可是事件写法的话就不能够sleep了(和上面同步请求同样会阻塞)。
因而Tornado提供了一个方法叫IOLoop.instance().add_timeout(),这个方法吃一个回调函数,而后能够指定在某时间或通过多长时间后执行这个函数。因而咱们让它吃本身就能够了。 框架
1
2
3
4
5
6
7
8
9
10
11
|
@tornado.gen.coroutine
defget_btce():
globalcurrent
res=yieldtornado.httpclient.AsyncHTTPClient().fetch(
"https://btc-e.com/api/2/ltc_btc/ticker"
)
current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']
tornado.ioloop.IOLoop.instance().add_timeout(
datetime.timedelta(milliseconds=delta),
get_btce()
)
|
可是呢,每次写起来会很麻烦,本着“以代码重用为荣,以复制粘贴为耻”的精神,咱们把这个功能抽出来,用有着“Python两大黑科技”的修饰器(另外一个是metaclass)来实现。 函数
1
2
3
4
5
6
7
8
9
10
11
|
defloop_call(delta=60*1000):
defdecorator(func):
@functools.wraps(func)
defwrapper(*args,**kwargs):
func(*args,**kwargs)
tornado.ioloop.IOLoop.instance().add_timeout(
datetime.timedelta(milliseconds=delta),
wrapper,
)
returnwrapper
returndecorator
|
这是一个典型的三层(带参)修饰器写法,使用以下(注意@coroutine要放在@loop_call下面)
1
2
3
4
5
6
7
8
|
@loop_call()
@tornado.gen.coroutine
defget_btce():
globalcurrent
res=yieldtornado.httpclient.AsyncHTTPClient().fetch(
"https://btc-e.com/api/2/ltc_btc/ticker"
)
current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']
|
修饰器分为带参和无参两种,前者使用@deco()调用,后者使用@deco ;前者在被应用的时候会调用deco()函数,这个函数必须返回一个无参修饰器(也就是decorator()那一层)
最外层的函数的函数名用作(带参)修饰器的名字,用于产生一个修饰器;
中间层的函数是一个无参修饰器,用于被带参修饰器返回;
最里层的函数是被修饰以后的新函数,总使用@functools.wraps(原函数)修饰,这个函数用于把原来函数的一些属性(__name__、__doc__一类)应用到新建的函数(wrapper)上。
修饰器应用流程:
以缺省参数delta=60*1000执行loop_call(),返回一个新函数decorator();
以参数func=get_btce执行decorator(),返回被wrap(get_btce)修饰的新函数wrapper做为新的get_btce();
在get_btce()被调用时,会先执行func(也就是原来的get_btce()),而后用最外层的delta参数调用add_timeout()
整体来讲效果就是这样的,其他部分都是sample级的代码因此很少说。
import tornado.web import tornado.gen import tornado.httputil import tornado.escape import tornado.httpclient import datetime import functools current = {} def loop_call(delta=60*1000): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) tornado.ioloop.IOLoop.instance().add_timeout( datetime.timedelta(milliseconds=delta), wrapper, ) return wrapper return decorator # These two decorators must be applied in order @loop_call() @tornado.gen.coroutine def get_btce(): global current res = yield tornado.httpclient.AsyncHTTPClient().fetch( "https://btc-e.com/api/2/ltc_btc/ticker" ) current['btce'] = tornado.escape.json_decode(res.body)['ticker']['last'] @loop_call() @tornado.gen.coroutine def get_btcc(): global current res = yield tornado.httpclient.AsyncHTTPClient().fetch( "https://data.btcchina.com/data/ticker" ) current['btcc'] = tornado.escape.json_decode(res.body)['ticker']['last'] @loop_call() @tornado.gen.coroutine def get_diff(): global current res = yield tornado.httpclient.AsyncHTTPClient().fetch( "http://api.ltcd.info/difficulty" ) current['diff'] = tornado.escape.json_decode(res.body)['current-difficulty'] class TickerHandler(tornado.web.RequestHandler): def get(self,key): global current self.write(str(current.get(key,"N/A"))) class RootHandler(tornado.web.RequestHandler): def get(self): self.write("Ticker") tornado.web.Application([ (r'/',RootHandler), (r'/(.*)',TickerHandler), ],debug=True).listen(8733) get_btcc() get_btce() get_diff() tornado.ioloop.IOLoop.instance().start()