無論你們認不認識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()