日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
PythonWeb應(yīng)用程序Tornado框架簡介

在比較 Python 框架的系列文章的第三部分中,我們來了解 Tornado,它是為處理異步進(jìn)程而構(gòu)建的。

成都創(chuàng)新互聯(lián)長期為上千家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為巴南企業(yè)提供專業(yè)的網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)巴南網(wǎng)站改版等技術(shù)服務(wù)。擁有10多年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。

在這個(gè)由四部分組成的系列文章的前兩篇中,我們介紹了 Pyramid 和 Flask Web 框架。我們已經(jīng)構(gòu)建了兩次相同的應(yīng)用程序,看到了一個(gè)完整的 DIY 框架和包含了更多功能的框架之間的異同。

現(xiàn)在讓我們來看看另一個(gè)稍微不同的選擇:Tornado 框架。Tornado 在很大程度上與 Flask 一樣簡單,但有一個(gè)主要區(qū)別:Tornado 是專門為處理異步進(jìn)程而構(gòu)建的。在我們本系列所構(gòu)建的應(yīng)用程序中,這種特殊的醬料(LCTT 譯注:這里意思是 Tornado 的異步功能)在我們構(gòu)建的 app 中并不是非常有用,但我們將看到在哪里可以使用它,以及它在更一般的情況下是如何工作的。

讓我們繼續(xù)前兩篇文章中模式,首先從處理設(shè)置和配置開始。

Tornado 啟動和配置

如果你一直關(guān)注這個(gè)系列,那么第一步應(yīng)該對你來說習(xí)以為常。

 
 
 
  1. $ mkdir tornado_todo
  2. $ cd tornado_todo
  3. $ pipenv install --python 3.6
  4. $ pipenv shell
  5. (tornado-someHash) $ pipenv install tornado

創(chuàng)建一個(gè) setup.py 文件來安裝我們的應(yīng)用程序相關(guān)的東西:

 
 
 
  1. (tornado-someHash) $ touch setup.py
  2. # setup.py
  3. from setuptools import setup, find_packages
  4. requires = [
  5.     'tornado',
  6.     'tornado-sqlalchemy',
  7.     'psycopg2',
  8. ]
  9. setup(
  10.     name='tornado_todo',
  11.     version='0.0',
  12.     description='A To-Do List built with Tornado',
  13.     author='',
  14.     author_email='',
  15.     keywords='web tornado',
  16.     packages=find_packages(),
  17.     install_requires=requires,
  18.     entry_points={
  19.         'console_scripts': [
  20.             'serve_app = todo:main',
  21.         ],
  22.     },
  23. )

因?yàn)?Tornado 不需要任何外部配置,所以我們可以直接編寫 Python 代碼來讓程序運(yùn)行。讓我們創(chuàng)建 todo 目錄,并用需要的前幾個(gè)文件填充它。

 
 
 
  1. todo/
  2.     __init__.py
  3.     models.py
  4.     views.py

就像 Flask 和 Pyramid 一樣,Tornado 也有一些基本配置,放在 __init__.py 中。從 tornado.web 中,我們將導(dǎo)入 Application 對象,它將處理路由和視圖的連接,包括數(shù)據(jù)庫(當(dāng)我們談到那里時(shí)再說)以及運(yùn)行 Tornado 應(yīng)用程序所需的其它額外設(shè)置。

 
 
 
  1. # __init__.py
  2. from tornado.web import Application
  3. def main():
  4. """Construct and serve the tornado application."""
  5. app = Application()

像 Flask 一樣,Tornado 主要是一個(gè) DIY 框架。當(dāng)構(gòu)建我們的 app 時(shí),我們必須設(shè)置該應(yīng)用實(shí)例。因?yàn)?Tornado 用它自己的 HTTP 服務(wù)器來提供該應(yīng)用,我們必須設(shè)置如何提供該應(yīng)用。首先,在 tornado.options.define 中定義要監(jiān)聽的端口。然后我們實(shí)例化 Tornado 的 HTTPServer,將該 Application 對象的實(shí)例作為參數(shù)傳遞給它。

 
 
 
  1. # __init__.py
  2. from tornado.httpserver import HTTPServer
  3. from tornado.options import define, options
  4. from tornado.web import Application
  5. define('port', default=8888, help='port to listen on')
  6. def main():
  7.     """Construct and serve the tornado application."""
  8.     app = Application()
  9.     http_server = HTTPServer(app)
  10.     http_server.listen(options.port)

當(dāng)我們使用 define 函數(shù)時(shí),我們最終會在 options 對象上創(chuàng)建屬性。第一個(gè)參數(shù)位置的任何內(nèi)容都將是屬性的名稱,分配給 default 關(guān)鍵字參數(shù)的內(nèi)容將是該屬性的值。

例如,如果我們將屬性命名為 potato 而不是 port,我們可以通過 options.potato 訪問它的值。

HTTPServer 上調(diào)用 listen 并不會啟動服務(wù)器。我們必須再做一步,找一個(gè)可以監(jiān)聽請求并返回響應(yīng)的工作應(yīng)用程序,我們需要一個(gè)輸入輸出循環(huán)。幸運(yùn)的是,Tornado 以 tornado.ioloop.IOLoop 的形式提供了開箱即用的功能。

 
 
 
  1. # __init__.py
  2. from tornado.httpserver import HTTPServer
  3. from tornado.ioloop import IOLoop
  4. from tornado.options import define, options
  5. from tornado.web import Application
  6. define('port', default=8888, help='port to listen on')
  7. def main():
  8.     """Construct and serve the tornado application."""
  9.     app = Application()
  10.     http_server = HTTPServer(app)
  11.     http_server.listen(options.port)
  12.     print('Listening on http://localhost:%i' % options.port)
  13.     IOLoop.current().start()

我喜歡某種形式的 print 語句,來告訴我什么時(shí)候應(yīng)用程序正在提供服務(wù),這是我的習(xí)慣。如果你愿意,可以不使用 print。

我們以 IOLoop.current().start() 開始我們的 I/O 循環(huán)。讓我們進(jìn)一步討論輸入,輸出和異步性。

Python 中的異步和 I/O 循環(huán)的基礎(chǔ)知識

請?jiān)试S我提前說明,我絕對,肯定,一定并且放心地說不是異步編程方面的專家。就像我寫的所有內(nèi)容一樣,接下來的內(nèi)容源于我對這個(gè)概念的理解的局限性。因?yàn)槲沂侨?,可能有很深很深的缺陷?/p>

異步程序的主要問題是:

 
 
 
  1. * 數(shù)據(jù)如何進(jìn)來?
  2. * 數(shù)據(jù)如何出去?
  3. * 什么時(shí)候可以在不占用我全部注意力情況下運(yùn)行某個(gè)過程?

由于全局解釋器鎖(GIL),Python 被設(shè)計(jì)為一種單線程語言。對于 Python 程序必須執(zhí)行的每個(gè)任務(wù),其線程執(zhí)行的全部注意力都集中在該任務(wù)的持續(xù)時(shí)間內(nèi)。我們的 HTTP 服務(wù)器是用 Python 編寫的,因此,當(dāng)接收到數(shù)據(jù)(如 HTTP 請求)時(shí),服務(wù)器的唯一關(guān)心的是傳入的數(shù)據(jù)。這意味著,在大多數(shù)情況下,無論是程序需要運(yùn)行還是處理數(shù)據(jù),程序都將完全消耗服務(wù)器的執(zhí)行線程,阻止接收其它可能的數(shù)據(jù),直到服務(wù)器完成它需要做的事情。

在許多情況下,這不是太成問題。典型的 Web 請求,響應(yīng)周期只需要幾分之一秒。除此之外,構(gòu)建 HTTP 服務(wù)器的套接字可以維護(hù)待處理的傳入請求的積壓。因此,如果請求在該套接字處理其它內(nèi)容時(shí)進(jìn)入,則它很可能只是在處理之前稍微排隊(duì)等待一會。對于低到中等流量的站點(diǎn),幾分之一秒的時(shí)間并不是什么大問題,你可以使用多個(gè)部署的實(shí)例以及 NGINX 等負(fù)載均衡器來為更大的請求負(fù)載分配流量。

但是,如果你的平均響應(yīng)時(shí)間超過一秒鐘,該怎么辦?如果你使用來自傳入請求的數(shù)據(jù)來啟動一些長時(shí)間的過程(如機(jī)器學(xué)習(xí)算法或某些海量數(shù)據(jù)庫查詢),該怎么辦?現(xiàn)在,你的單線程 Web 服務(wù)器開始累積一個(gè)無法尋址的積壓請求,其中一些請求會因?yàn)槌瑫r(shí)而被丟棄。這不是一種選擇,特別是如果你希望你的服務(wù)在一段時(shí)間內(nèi)是可靠的。

異步 Python 程序登場。重要的是要記住因?yàn)樗怯?Python 編寫的,所以程序仍然是一個(gè)單線程進(jìn)程。除非特別標(biāo)記,否則在異步程序中仍然會阻塞執(zhí)行。

但是,當(dāng)異步程序結(jié)構(gòu)正確時(shí),只要你指定某個(gè)函數(shù)應(yīng)該具有這樣的能力,你的異步 Python 程序就可以“擱置”長時(shí)間運(yùn)行的任務(wù)。然后,當(dāng)擱置的任務(wù)完成并準(zhǔn)備好恢復(fù)時(shí),異步控制器會收到報(bào)告,只要在需要時(shí)管理它們的執(zhí)行,而不會完全阻塞對新輸入的處理。

這有點(diǎn)夸張,所以讓我們用一個(gè)人類的例子來證明。

帶回家吧

我經(jīng)常發(fā)現(xiàn)自己在家里試圖完成很多家務(wù),但沒有多少時(shí)間來做它們。在某一天,積壓的家務(wù)可能看起來像:

 
 
 
  1. * 做飯(20 分鐘準(zhǔn)備,40 分鐘烹飪)
  2. * 洗碗(60 分鐘)
  3. * 洗滌并擦干衣物(30 分鐘洗滌,每次干燥 90 分鐘)
  4. * 真空清洗地板(30 分鐘)

如果我是一個(gè)傳統(tǒng)的同步程序,我會親自完成每項(xiàng)任務(wù)。在我考慮處理任何其他事情之前,每項(xiàng)任務(wù)都需要我全神貫注地完成。因?yàn)槿绻麤]有我的全力關(guān)注,什么事情都完成不了。所以我的執(zhí)行順序可能如下:

 
 
 
  1. 1. 完全專注于準(zhǔn)備和烹飪食物,包括等待食物烹飪(60 分鐘)
  2. 2. 將臟盤子移到水槽中(65 分鐘過去了)
  3. 3. 清洗所有盤子(125 分鐘過去了)
  4. 4. 開始完全專注于洗衣服,包括等待洗衣機(jī)洗完,然后將衣物轉(zhuǎn)移到烘干機(jī),再等烘干機(jī)完成( 250 分鐘過去了)
  5. 5. 對地板進(jìn)行真空吸塵(280 分鐘了)

從頭到尾完成所有事情花費(fèi)了 4 小時(shí) 40 分鐘。

我應(yīng)該像異步程序一樣聰明地工作,而不是努力工作。我的家里到處都是可以為我工作的機(jī)器,而不用我一直努力工作。同時(shí),現(xiàn)在我可以將注意力轉(zhuǎn)移真正需要的東西上。

我的執(zhí)行順序可能看起來像:

 
 
 
  1. 1. 將衣物放入洗衣機(jī)并啟動它(5 分鐘)
  2. 2. 在洗衣機(jī)運(yùn)行時(shí),準(zhǔn)備食物(25 分鐘過去了)
  3. 3. 準(zhǔn)備好食物后,開始烹飪食物(30 分鐘過去了)
  4. 4. 在烹飪食物時(shí),將衣物從洗衣機(jī)移到烘干機(jī)機(jī)中開始烘干(35 分鐘過去了)
  5. 5. 當(dāng)烘干機(jī)運(yùn)行中,且食物仍在烹飪時(shí),對地板進(jìn)行真空吸塵(65 分鐘過去了)
  6. 6. 吸塵后,將食物從爐子中取出并裝盤子入洗碗機(jī)(70 分鐘過去了)
  7. 7. 運(yùn)行洗碗機(jī)(130 分鐘完成)

現(xiàn)在花費(fèi)的時(shí)間下降到 2 小時(shí) 10 分鐘。即使我允許在作業(yè)之間切換花費(fèi)更多時(shí)間(總共 10-20 分鐘)。如果我等待著按順序執(zhí)行每項(xiàng)任務(wù),我花費(fèi)的時(shí)間仍然只有一半左右。這就是將程序構(gòu)造為異步的強(qiáng)大功能。

那么 I/O 循環(huán)在哪里?

一個(gè)異步 Python 程序的工作方式是從某個(gè)外部源(輸入)獲取數(shù)據(jù),如果某個(gè)進(jìn)程需要,則將該數(shù)據(jù)轉(zhuǎn)移到某個(gè)外部工作者(輸出)進(jìn)行處理。當(dāng)外部進(jìn)程完成時(shí),Python 主程序會收到提醒,然后程序獲取外部處理(輸入)的結(jié)果,并繼續(xù)這樣其樂融融的方式。

當(dāng)數(shù)據(jù)不在 Python 主程序手中時(shí),主程序就會被釋放來處理其它任何事情。包括等待全新的輸入(如 HTTP 請求)和處理長時(shí)間運(yùn)行的進(jìn)程的結(jié)果(如機(jī)器學(xué)習(xí)算法的結(jié)果,長時(shí)間運(yùn)行的數(shù)據(jù)庫查詢)。主程序雖仍然是單線程的,但成了事件驅(qū)動的,它對程序處理的特定事件會觸發(fā)動作。監(jiān)聽這些事件并指示應(yīng)如何處理它們的主要是 I/O 循環(huán)在工作。

我知道,我們走了很長的路才得到這個(gè)重要的解釋,但我希望在這里傳達(dá)的是,它不是魔術(shù),也不是某種復(fù)雜的并行處理或多線程工作。全局解釋器鎖仍然存在,主程序中任何長時(shí)間運(yùn)行的進(jìn)程仍然會阻塞其它任何事情的進(jìn)行,該程序仍然是單線程的。然而,通過將繁瑣的工作外部化,我們可以將線程的注意力集中在它需要注意的地方。

這有點(diǎn)像我上面的異步任務(wù)。當(dāng)我的注意力完全集中在準(zhǔn)備食物上時(shí),它就是我所能做的一切。然而,當(dāng)我能讓爐子幫我做飯,洗碗機(jī)幫我洗碗,洗衣機(jī)和烘干機(jī)幫我洗衣服時(shí),我的注意力就會被釋放出來,去做其它事情。當(dāng)我被提醒,我的一個(gè)長時(shí)間運(yùn)行的任務(wù)已經(jīng)完成并準(zhǔn)備再次處理時(shí),如果我的注意力是空閑的,我可以獲取該任務(wù)的結(jié)果,并對其做下一步需要做的任何事情。

Tornado 路由和視圖

盡管經(jīng)歷了在 Python 中討論異步的所有麻煩,我們還是決定暫不使用它。先來編寫一個(gè)基本的 Tornado 視圖。

與我們在 Flask 和 Pyramid 實(shí)現(xiàn)中看到的基于函數(shù)的視圖不同,Tornado 的視圖都是基于類的。這意味著我們將不在使用單獨(dú)的、獨(dú)立的函數(shù)來規(guī)定如何處理請求。相反,傳入的 HTTP 請求將被捕獲并將其分配為我們定義的類的一個(gè)屬性。然后,它的方法將處理相應(yīng)的請求類型。

讓我們從一個(gè)基本的視圖開始,即在屏幕上打印 “Hello, World”。我們?yōu)?Tornado 應(yīng)用程序構(gòu)造的每個(gè)基于類的視圖都必須繼承 tornado.web 中的 RequestHandler 對象。這將設(shè)置我們需要(但不想寫)的所有底層邏輯來接收請求,同時(shí)構(gòu)造正確格式的 HTTP 響應(yīng)。

 
 
 
  1. from tornado.web import RequestHandler
  2. class HelloWorld(RequestHandler):
  3.     """Print 'Hello, world!' as the response body."""
  4.     def get(self):
  5.         """Handle a GET request for saying Hello World!."""
  6.         self.write("Hello, world!")

因?yàn)槲覀円幚?GET 請求,所以我們聲明(實(shí)際上是重寫)了 get 方法。我們提供文本或 JSON 可序列化對象,用 self.write 寫入響應(yīng)體。之后,我們讓 RequestHandler 來做在發(fā)送響應(yīng)之前必須完成的其它工作。

就目前而言,此視圖與 Tornado 應(yīng)用程序本身并沒有實(shí)際連接。我們必須回到 __init__.py,并稍微更新 main 函數(shù)。以下是新的內(nèi)容:

 
 
 
  1. # __init__.py
  2. from tornado.httpserver import HTTPServer
  3. from tornado.ioloop import IOLoop
  4. from tornado.options import define, options
  5. from tornado.web import Application
  6. from todo.views import HelloWorld
  7. define('port', default=8888, help='port to listen on')
  8. def main():
  9.     """Construct and serve the tornado application."""
  10.     app = Application([
  11.         ('/', HelloWorld)
  12.     ])
  13.     http_server = HTTPServer(app)
  14.     http_server.listen(options.port)
  15.     print('Listening on http://localhost:%i' % options.port)
  16.     IOLoop.current().start()

我們做了什么

我們將 views.py 文件中的 HelloWorld 視圖導(dǎo)入到腳本 __init__.py 的頂部。然后我們添加了一個(gè)路由-視圖對應(yīng)的列表,作為 Application 實(shí)例化的第一個(gè)參數(shù)。每當(dāng)我們想要在應(yīng)用程序中聲明一個(gè)路由時(shí),它必須綁定到一個(gè)視圖。如果需要,可以對多個(gè)路由使用相同的視圖,但每個(gè)路由必須有一個(gè)視圖。

我們可以通過在 setup.py 中啟用的 serve_app 命令來運(yùn)行應(yīng)用程序,從而確保這一切都能正常工作。查看 http://localhost:8888/ 并看到它顯示 “Hello, world!”。

當(dāng)然,在這個(gè)領(lǐng)域中我們還能做更多,也將做更多,但現(xiàn)在讓我們來討論模型吧。

連接數(shù)據(jù)庫

如果我們想要保留數(shù)據(jù),就需要連接數(shù)據(jù)庫。與 Flask 一樣,我們將使用一個(gè)特定于框架的 SQLAchemy 變體,名為 tornado-sqlalchemy。

為什么要使用它而不是 SQLAlchemy 呢?好吧,其實(shí) tornado-sqlalchemy 具有簡單 SQLAlchemy 的所有優(yōu)點(diǎn),因此我們?nèi)匀豢梢允褂猛ㄓ玫?Base 聲明模型,并使用我們習(xí)以為常的所有列數(shù)據(jù)類型和關(guān)系。除了我們已經(jīng)慣常了解到的,tornado-sqlalchemy 還為其數(shù)據(jù)庫查詢功能提供了一種可訪問的異步模式,專門用于與 Tornado 現(xiàn)有的 I/O 循環(huán)一起工作。

我們通過將 tornado-sqlalchemypsycopg2 添加到 setup.py 到所需包的列表并重新安裝包來創(chuàng)建環(huán)境。在 models.py 中,我們聲明了模型。這一步看起來與我們在 Flask 和 Pyramid 中已經(jīng)看到的完全一樣,所以我將跳過全部聲明,只列出了 Task 模型的必要部分。

 
 
 
  1. # 這不是完整的 models.py, 但是足夠看到不同點(diǎn)
  2. from tornado_sqlalchemy import declarative_base
  3. Base = declarative_base
  4. class Task(Base):
  5.     # 等等,因?yàn)槭O碌膸缀跛械臇|西都一樣 ...

我們?nèi)匀恍枰獙?tornado-sqlalchemy 連接到實(shí)際應(yīng)用程序。在 __init__.py 中,我們將定義數(shù)據(jù)庫并將其集成到應(yīng)用程序中。

 
 
 
  1. # __init__.py
  2. from tornado.httpserver import HTTPServer
  3. from tornado.ioloop import IOLoop
  4. from tornado.options import define, options
  5. from tornado.web import Application
  6. from todo.views import HelloWorld
  7. # add these
  8. import os
  9. from tornado_sqlalchemy import make_session_factory
  10. define('port', default=8888, help='port to listen on')
  11. factory = make_session_factory(os.environ.get('DATABASE_URL', ''))
  12. def main():
  13.     """Construct and serve the tornado application."""
  14.     app = Application([
  15.         ('/', HelloWorld)
  16.     ],
  17.         session_factory=factory
  18.     )
  19.     http_server = HTTPServer(app)
  20.     http_server.listen(options.port)
  21.     print('Listening on http://localhost:%i' % options.port)
  22.     IOLoop.current().start()

就像我們在 Pyramid 中傳遞的會話工廠一樣,我們可以使用 make_session_factory 來接收數(shù)據(jù)庫 URL 并生成一個(gè)對象,這個(gè)對象的唯一目的是為視圖提供到數(shù)據(jù)庫的連接。然后我們將新創(chuàng)建的 factory 傳遞給 Application 對象,并使用 session_factory 關(guān)鍵字參數(shù)將它綁定到應(yīng)用程序中。

最后,初始化和管理數(shù)據(jù)庫與 Flask 和 Pyramid 相同(即,單獨(dú)的 DB 管理腳本,與 Base 對象一起工作等)。它看起來很相似,所以在這里我就不介紹了。

回顧視圖

Hello,World 總是適合學(xué)習(xí)基礎(chǔ)知識,但我們需要一些真實(shí)的,特定應(yīng)用程序的視圖。

讓我們從 info 視圖開始。

 
 
 
  1. # views.py
  2. import json
  3. from tornado.web import RequestHandler
  4. class InfoView(RequestHandler):
  5.     """只允許 GET 請求"""
  6.     SUPPORTED_METHODS = ["GET"]
  7.     def set_default_headers(self):
  8.         """設(shè)置默認(rèn)響應(yīng)頭為 json 格式的"""
  9.         self.set_header("Content-Type", 'application/json; charset="utf-8"')
  10.     def get(self):
  11.         """列出這個(gè) API 的路由"""
  12.         routes = {
  13.             'info': 'GET /api/v1',
  14.             'register': 'POST /api/v1/accounts',
  15.             'single profile detail': 'GET /api/v1/accounts/',
  16.             'edit profile': 'PUT /api/v1/accounts/',
  17.             'delete profile': 'DELETE /api/v1/accounts/',
  18.             'login': 'POST /api/v1/accounts/login',
  19.             'logout': 'GET /api/v1/accounts/logout',
  20.             "user's tasks": 'GET /api/v1/accounts//tasks',
  21.             "create task": 'POST /api/v1/accounts//tasks',
  22.             "task detail": 'GET /api/v1/accounts//tasks/',
  23.             "task update": 'PUT /api/v1/accounts//tasks/',
  24.             "delete task": 'DELETE /api/v1/accounts//tasks/'
  25.         }
  26.         self.write(json.dumps(routes))

有什么改變嗎?讓我們從上往下看。

我們添加了 SUPPORTED_METHODS 類屬性,它是一個(gè)可迭代對象,代表這個(gè)視圖所接受的請求方法,其他任何方法都將返回一個(gè) 405 狀態(tài)碼。當(dāng)我們創(chuàng)建 HelloWorld 視圖時(shí),我們沒有指定它,主要是當(dāng)時(shí)有點(diǎn)懶。如果沒有這個(gè)類屬性,此視圖將響應(yīng)任何試圖綁定到該視圖的路由的請求。

我們聲明了 set_default_headers 方法,它設(shè)置 HTTP 響應(yīng)的默認(rèn)頭。我們在這里聲明它,以確保我們返回的任何響應(yīng)都有一個(gè) "Content-Type""application/json" 類型。

我們將 json.dumps(some_object) 添加到 self.write 的參數(shù)中,因?yàn)樗梢院苋菀椎貥?gòu)建響應(yīng)主體的內(nèi)容。

現(xiàn)在已經(jīng)完成了,我們可以繼續(xù)將它連接到 __init__.py 中的主路由。

 
 
 
  1. # __init__.py
  2. from tornado.httpserver import HTTPServer
  3. from tornado.ioloop import IOLoop
  4. from tornado.options import define, options
  5. from tornado.web import Application
  6. from todo.views import InfoView
  7. # 添加這些
  8. import os
  9. from tornado_sqlalchemy import make_session_factory
  10. define('port', default=8888, help='port to listen on')
  11. factory = make_session_factory(os.environ.get('DATABASE_URL', ''))
  12. def main():
  13.     """Construct and serve the tornado application."""
  14.     app = Application([
  15.         ('/', InfoView)
  16.     ],
  17.         session_factory=factory
  18.     )
  19.     http_server = HTTPServer(app)
  20.     http_server.listen(options.port)
  21.     print('Listening on http://localhost:%i' % options.port)
  22.     IOLoop.current().start()

我們知道,還需要編寫更多的視圖和路由。每個(gè)都會根據(jù)需要放入 Application 路由列表中,每個(gè)視圖還需要一個(gè) set_default_headers 方法。在此基礎(chǔ)上,我們還將創(chuàng)建 send_response 方法,它的作用是將響應(yīng)與我們想要給響應(yīng)設(shè)置的任何自定義狀態(tài)碼打包在一起。由于每個(gè)視圖都需要這兩個(gè)方法,因此我們可以創(chuàng)建一個(gè)包含它們的基類,這樣每個(gè)視圖都可以繼承基類。這樣,我們只需要編寫一次。

 
 
 
  1. # views.py
  2. import json
  3. from tornado.web import RequestHandler
  4. class BaseView(RequestHandler):
  5.     """Base view for this application."""
  6.     def set_default_headers(self):
  7.         """Set the default response header to be JSON."""
  8.         self.set_header("Content-Type", 'application/json; charset="utf-8"')
  9.     def send_response(self, data, status=200):
  10.         """Construct and send a JSON response with appropriate status code."""
  11.         self.set_status(status)
  12.         self.write(json.dumps(data))

對于我們即將編寫的 TaskListView 這樣的視圖,我們還需要一個(gè)到數(shù)據(jù)庫的連接。我們需要 tornado_sqlalchemy 中的 SessionMixin 在每個(gè)視圖類中添加一個(gè)數(shù)據(jù)庫會話。我們可以將它放在 BaseView 中,這樣,默認(rèn)情況下,從它繼承的每個(gè)視圖都可以訪問數(shù)據(jù)庫會話。

 
 
 
  1. # views.py
  2. import json
  3. from tornado_sqlalchemy import SessionMixin
  4. from tornado.web import RequestHandler
  5. class BaseView(RequestHandler, SessionMixin):
  6.     """Base view for this application."""
  7.     def set_default_headers(self):
  8.         """Set the default response header to be JSON."""
  9.         self.set_header("Content-Type", 'application/json; charset="utf-8"')
  10.     def send_response(self, data, status=200):
  11.         """Construct and send a JSON response with appropriate status code."""
  12.         self.set_status(status)
  13.         self.write(json.dumps(data))

只要我們修改 BaseView 對象,在將數(shù)據(jù)發(fā)布到這個(gè) API 時(shí),我們就應(yīng)該定位到這里。

當(dāng) Tornado(從 v.4.5 開始)使用來自客戶端的數(shù)據(jù)并將其組織起來到應(yīng)用程序中使用時(shí),它會將所有傳入數(shù)據(jù)視為字節(jié)串。但是,這里的所有代碼都假設(shè)使用 Python 3,因此我們希望使用的唯一字符串是 Unicode 字符串。我們可以為這個(gè) BaseView 類添加另一個(gè)方法,它的工作是將輸入數(shù)據(jù)轉(zhuǎn)換為 Unicode,然后再在視圖的其他地方使用。

如果我們想要在正確的視圖方法中使用它之前轉(zhuǎn)換這些數(shù)據(jù),我們可以重寫視圖類的原生 prepare 方法。它的工作是在視圖方法運(yùn)行前運(yùn)行。如果我們重寫 prepare 方法,我們可以設(shè)置一些邏輯來運(yùn)行,每當(dāng)收到請求時(shí),這些邏輯就會執(zhí)行字節(jié)串到 Unicode 的轉(zhuǎn)換。

 
 
 
  1. # views.py
  2. import json
  3. from tornado_sqlalchemy import SessionMixin
  4. from tornado.web import RequestHandler
  5. class BaseView(RequestHandler, SessionMixin):
  6.     """Base view for this application."""
  7.     def prepare(self):
  8.         self.form_data = {
  9.             key: [val.decode('utf8') for val in val_list]
  10.             for key, val_list in self.request.arguments.items()
  11.         }
  12.     def set_default_headers(self):
  13.         """Set the default response header to be JSON."""
  14.         self.set_header("Content-Type", 'application/json; charset="utf-8"')
  15.     def send_response(self, data, status=200):
  16.         """Construct and send a JSON response with appropriate status code."""
  17.         self.set_status(status)
  18.         self.write(json.dumps(data))

如果有任何數(shù)據(jù)進(jìn)入,它將在 self.request.arguments 字典中找到。我們可以通過鍵訪問該數(shù)據(jù)庫,并將其內(nèi)容(始終是列表)轉(zhuǎn)換為 Unicode。因?yàn)檫@是基于類的視圖而不是基于函數(shù)的,所以我們可以將修改后的數(shù)據(jù)存儲為一個(gè)實(shí)例屬性,以便以后使用。我在這里稱它為 form_data,但它也可以被稱為 potato。關(guān)鍵是我們可以存儲提交給應(yīng)用程序的數(shù)據(jù)。

異步視圖方法

現(xiàn)在我們已經(jīng)構(gòu)建了 BaseaView,我們可以構(gòu)建 TaskListView 了,它會繼承 BaseaView。

正如你可以從章節(jié)標(biāo)題中看到的那樣,以下是所有關(guān)于異步性的討論。TaskListView 將處理返回任務(wù)列表的 GET 請求和用戶給定一些表單數(shù)據(jù)來創(chuàng)建新任務(wù)的 POST 請求。讓我們首先來看看處理 GET 請求的代碼。

 
 
 
  1. # all the previous imports
  2. import datetime
  3. from tornado.gen import coroutine
  4. from tornado_sqlalchemy import as_future
  5. from todo.models import Profile, Task
  6. # the BaseView is above here
  7. class TaskListView(BaseView):
  8.     """View for reading and adding new tasks."""
  9.     SUPPORTED_METHODS = ("GET", "POST",)
  10.     @coroutine
  11.     def get(self, username):
  12.         """Get all tasks for an existing user."""
  13.         with self.make_session() as session:
  14.             profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)
  15.             if profile:
  16.                 tasks = [task.to_dict() for task in profile.tasks]
  17.                 self.send_response({
  18.                     'username': profile.username,
  19.                     'tasks': tasks
  20.                 })

這里的第一個(gè)主要部分是 @coroutine 裝飾器,它從 tornado.gen 導(dǎo)入。任何具有與調(diào)用堆棧的正常流程不同步的 Python 可調(diào)用部分實(shí)際上是“協(xié)程”,即一個(gè)可以與其它協(xié)程一起運(yùn)行的協(xié)程。在我的家務(wù)勞動的例子中,幾乎所有的家務(wù)活都是一個(gè)共同的例行協(xié)程。有些阻止了例行協(xié)程(例如,給地板吸塵),但這種例行協(xié)程只會阻礙我開始或關(guān)心其它任何事情的能力。它沒有阻止已經(jīng)啟動的任何其他協(xié)程繼續(xù)進(jìn)行。

Tornado 提供了許多方法來構(gòu)建一個(gè)利用協(xié)程的應(yīng)用程序,包括允許我們設(shè)置函數(shù)調(diào)用鎖,同步異步協(xié)程的條件,以及手動修改控制 I/O 循環(huán)的事件系統(tǒng)。

這里使用 @coroutine 裝飾器的唯一條件是允許 get 方法將 SQL 查詢作為后臺進(jìn)程,并在查詢完成后恢復(fù),同時(shí)不阻止 Tornado I/O 循環(huán)去處理其他傳入的數(shù)據(jù)源。這就是關(guān)于此實(shí)現(xiàn)的所有“異步”:帶外數(shù)據(jù)庫查詢。顯然,如果我們想要展示異步 Web 應(yīng)用程序的魔力和神奇,那么一個(gè)任務(wù)列表就不是好的展示方式。

但是,這就是我們正在構(gòu)建的,所以讓我們來看看方法如何利用 @coroutine 裝飾器。SessionMixin 混合到 BaseView 聲明中,為我們的視圖類添加了兩個(gè)方便的,支持?jǐn)?shù)據(jù)庫的屬性:sessionmake_session。它們的名字相似,實(shí)現(xiàn)的目標(biāo)也相當(dāng)相似。

self.session 屬性是一個(gè)關(guān)注數(shù)據(jù)庫的會話。在請求-響應(yīng)周期結(jié)束時(shí),在視圖將響應(yīng)發(fā)送回客戶端之前,任何對數(shù)據(jù)庫的更改都被提交,并關(guān)閉會話。

self.make_session 是一個(gè)上下文管理器和生成器,可以動態(tài)構(gòu)建和返回一個(gè)全新的會話對象。第一個(gè) self.session 對象仍然存在。無論如何,反正 make_session 會創(chuàng)建一個(gè)新的。make_session 生成器還為其自身提供了一個(gè)功能,用于在其上下文(即縮進(jìn)級別)結(jié)束時(shí)提交和關(guān)閉它創(chuàng)建的會話。

如果你查看源代碼,則賦值給 self.session 的對象類型與 self.make_session 生成的對象類型之間沒有區(qū)別,不同之處在于它們是如何被管理的。

使用 make_session 上下文管理器,生成的會話僅屬于上下文,在該上下文中開始和結(jié)束。你可以使用 make_session 上下文管理器在同一個(gè)視圖中打開,修改,提交以及關(guān)閉多個(gè)數(shù)據(jù)庫會話。

self.session 要簡單得多,當(dāng)你進(jìn)入視圖方法時(shí)會話已經(jīng)打開,在響應(yīng)被發(fā)送回客戶端之前會話就已提交。

雖然讀取文檔片段和 PyPI 示例都說明了上下文管理器的使用,但是沒有說明 self.session 對象或由 self.make_session 生成的 session 本質(zhì)上是不是異步的。當(dāng)我們啟動查詢時(shí),我們開始考慮內(nèi)置于 tornado-sqlalchemy 中的異步行為。

tornado-sqlalchemy 包為我們提供了 as_future 函數(shù)。它的工作是裝飾 tornado-sqlalchemy 會話構(gòu)造的查詢并 yield 其返回值。如果視圖方法用 @coroutine 裝飾,那么使用 yield as_future(query) 模式將使封裝的查詢成為一個(gè)異步后臺進(jìn)程。I/O 循環(huán)會接管等待查詢的返回值和 as_future 創(chuàng)建的 future 對象的解析。

要訪問 as_future(query) 的結(jié)果,你必須從它 yield。否則,你只能獲得一個(gè)未解析的生成器對象,并且無法對查詢執(zhí)行任何操作。

這個(gè)視圖方法中的其他所有內(nèi)容都與之前課堂上的類似,與我們在 Flask 和 Pyramid 中看到的內(nèi)容類似。

post 方法看起來非常相似。為了保持一致性,讓我們看一下 post 方法以及它如何處理用 BaseView 構(gòu)造的 self.form_data。

 
 
 
  1. @coroutine
  2. def post(self, username):
  3.     """Create a new task."""
  4.     with self.make_session() as session:
  5.         profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)
  6.         if profile:
  7.             due_date = self.form_data['due_date'][0]
  8.             task = Task(
  9.                 name=self.form_data['name'][0],
  10.                 note=self.form_data['note'][0],
  11.                 creation_date=datetime.now(),
  12.                 due_date=datetime.strptime(due_date, '%d/%m/%Y %H:%M:%S') if due_date else None,
  13.                 completed=self.form_data['completed'][0],
  14.                 profile_id=profile.id,
  15.                 profile=profile
  16.             )
  17.             session.add(task)
  18.             self.send_response({'msg': 'posted'}, status=201)

正如我所說,這是我們所期望的:

  * 與我們在 get 方法中看到的查詢模式相同   * 構(gòu)造一個(gè)新的 Task 對象的實(shí)例,用 form_data 的數(shù)據(jù)填充   * 添加新的 Task 對象(但不提交,因?yàn)樗缮舷挛墓芾砥魈幚恚。┑綌?shù)據(jù)庫會話   * 將響應(yīng)發(fā)送給客戶端

這樣我們就有了 Tornado web 應(yīng)用程序的基礎(chǔ)。其他內(nèi)容(例如,數(shù)據(jù)庫管理和更多完整應(yīng)用程序的視圖)實(shí)際上與我們在 Flask 和 Pyramid 應(yīng)用程序中看到的相同。

關(guān)于使用合適的工具完成合適的工作的一點(diǎn)想法

在我們繼續(xù)瀏覽這些 Web 框架時(shí),我們開始看到它們都可以有效地處理相同的問題。對于像這樣的待辦事項(xiàng)列表,任何框架都可以完成這項(xiàng)任務(wù)。但是,有些 Web 框架比其它框架更適合某些工作,這具體取決于對你來說什么“更合適”和你的需求。

雖然 Tornado 顯然和 Pyramid 或 Flask 一樣可以處理相同工作,但將它用于這樣的應(yīng)用程序?qū)嶋H上是一種浪費(fèi),這就像開車從家走一個(gè)街區(qū)(LCTT 譯注:這里意思應(yīng)該是從家開始走一個(gè)街區(qū)只需步行即可)。是的,它可以完成“旅行”的工作,但短途旅行不是你選擇汽車而不是自行車或者使用雙腳的原因。

根據(jù)文檔,Tornado 被稱為 “Python Web 框架和異步網(wǎng)絡(luò)庫”。在 Python Web 框架生態(tài)系統(tǒng)中很少有人喜歡它。如果你嘗試完成的工作需要(或?qū)闹蝎@益)以任何方式、形狀或形式的異步性,使用 Tornado。如果你的應(yīng)用程序需要處理多個(gè)長期連接,同時(shí)又不想犧牲太多性能,選擇 Tornado。如果你的應(yīng)用程序是多個(gè)應(yīng)用程序,并且需要線程感知以準(zhǔn)確處理數(shù)據(jù),使用 Tornado。這是它最有效的地方。

用你的汽車做“汽車的事情”,使用其他交通工具做其他事情。

向前看,進(jìn)行一些深度檢查

談到使用合適的工具來完成合適的工作,在選擇框架時(shí),請記住應(yīng)用程序的范圍和規(guī)模,包括現(xiàn)在和未來。到目前為止,我們只研究了適用于中小型 Web 應(yīng)用程序的框架。本系列的下一篇也是最后一篇將介紹最受歡迎的 Python 框架之一 Django,它適用于可能會變得更大的大型應(yīng)用程序。同樣,盡管它在
文章名稱:PythonWeb應(yīng)用程序Tornado框架簡介
標(biāo)題鏈接:http://www.dlmjj.cn/article/dpsoodh.html