新聞中心
在比較 Python 框架的系列文章的第三部分中,我們來了解 Tornado,它是為處理異步進程而構建的。

成都創(chuàng)新互聯(lián)長期為上千家客戶提供的網站建設服務,團隊從業(yè)經驗10年,關注不同地域、不同群體,并針對不同對象提供差異化的產品和服務;打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網生態(tài)環(huán)境。為巴南企業(yè)提供專業(yè)的網站制作、成都網站設計,巴南網站改版等技術服務。擁有10多年豐富建站經驗和眾多成功案例,為您定制開發(fā)。
在這個由四部分組成的系列文章的前兩篇中,我們介紹了 Pyramid 和 Flask Web 框架。我們已經構建了兩次相同的應用程序,看到了一個完整的 DIY 框架和包含了更多功能的框架之間的異同。
現在讓我們來看看另一個稍微不同的選擇:Tornado 框架。Tornado 在很大程度上與 Flask 一樣簡單,但有一個主要區(qū)別:Tornado 是專門為處理異步進程而構建的。在我們本系列所構建的應用程序中,這種特殊的醬料(LCTT 譯注:這里意思是 Tornado 的異步功能)在我們構建的 app 中并不是非常有用,但我們將看到在哪里可以使用它,以及它在更一般的情況下是如何工作的。
讓我們繼續(xù)前兩篇文章中模式,首先從處理設置和配置開始。
Tornado 啟動和配置
如果你一直關注這個系列,那么第一步應該對你來說習以為常。
$ mkdir tornado_todo$ cd tornado_todo$ pipenv install --python 3.6$ pipenv shell(tornado-someHash) $ pipenv install tornado
創(chuàng)建一個 setup.py 文件來安裝我們的應用程序相關的東西:
(tornado-someHash) $ touch setup.py# setup.pyfrom setuptools import setup, find_packagesrequires = ['tornado','tornado-sqlalchemy','psycopg2',]setup(name='tornado_todo',version='0.0',description='A To-Do List built with Tornado',author='', author_email='', keywords='web tornado',packages=find_packages(),install_requires=requires,entry_points={'console_scripts': ['serve_app = todo:main',],},)
因為 Tornado 不需要任何外部配置,所以我們可以直接編寫 Python 代碼來讓程序運行。讓我們創(chuàng)建 todo 目錄,并用需要的前幾個文件填充它。
todo/__init__.pymodels.pyviews.py
就像 Flask 和 Pyramid 一樣,Tornado 也有一些基本配置,放在 __init__.py 中。從 tornado.web 中,我們將導入 Application 對象,它將處理路由和視圖的連接,包括數據庫(當我們談到那里時再說)以及運行 Tornado 應用程序所需的其它額外設置。
# __init__.pyfrom tornado.web import Applicationdef main():"""Construct and serve the tornado application."""app = Application()
像 Flask 一樣,Tornado 主要是一個 DIY 框架。當構建我們的 app 時,我們必須設置該應用實例。因為 Tornado 用它自己的 HTTP 服務器來提供該應用,我們必須設置如何提供該應用。首先,在 tornado.options.define 中定義要監(jiān)聽的端口。然后我們實例化 Tornado 的 HTTPServer,將該 Application 對象的實例作為參數傳遞給它。
# __init__.pyfrom tornado.httpserver import HTTPServerfrom tornado.options import define, optionsfrom tornado.web import Applicationdefine('port', default=8888, help='port to listen on')def main():"""Construct and serve the tornado application."""app = Application()http_server = HTTPServer(app)http_server.listen(options.port)
當我們使用 define 函數時,我們最終會在 options 對象上創(chuàng)建屬性。第一個參數位置的任何內容都將是屬性的名稱,分配給 default 關鍵字參數的內容將是該屬性的值。
例如,如果我們將屬性命名為 potato 而不是 port,我們可以通過 options.potato 訪問它的值。
在 HTTPServer 上調用 listen 并不會啟動服務器。我們必須再做一步,找一個可以監(jiān)聽請求并返回響應的工作應用程序,我們需要一個輸入輸出循環(huán)。幸運的是,Tornado 以 tornado.ioloop.IOLoop 的形式提供了開箱即用的功能。
# __init__.pyfrom tornado.httpserver import HTTPServerfrom tornado.ioloop import IOLoopfrom tornado.options import define, optionsfrom tornado.web import Applicationdefine('port', default=8888, help='port to listen on')def main():"""Construct and serve the tornado application."""app = Application()http_server = HTTPServer(app)http_server.listen(options.port)print('Listening on http://localhost:%i' % options.port)IOLoop.current().start()
我喜歡某種形式的 print 語句,來告訴我什么時候應用程序正在提供服務,這是我的習慣。如果你愿意,可以不使用 print。
我們以 IOLoop.current().start() 開始我們的 I/O 循環(huán)。讓我們進一步討論輸入,輸出和異步性。
Python 中的異步和 I/O 循環(huán)的基礎知識
請允許我提前說明,我絕對,肯定,一定并且放心地說不是異步編程方面的專家。就像我寫的所有內容一樣,接下來的內容源于我對這個概念的理解的局限性。因為我是人,可能有很深很深的缺陷。
異步程序的主要問題是:
* 數據如何進來?* 數據如何出去?* 什么時候可以在不占用我全部注意力情況下運行某個過程?
由于全局解釋器鎖(GIL),Python 被設計為一種單線程語言。對于 Python 程序必須執(zhí)行的每個任務,其線程執(zhí)行的全部注意力都集中在該任務的持續(xù)時間內。我們的 HTTP 服務器是用 Python 編寫的,因此,當接收到數據(如 HTTP 請求)時,服務器的唯一關心的是傳入的數據。這意味著,在大多數情況下,無論是程序需要運行還是處理數據,程序都將完全消耗服務器的執(zhí)行線程,阻止接收其它可能的數據,直到服務器完成它需要做的事情。
在許多情況下,這不是太成問題。典型的 Web 請求,響應周期只需要幾分之一秒。除此之外,構建 HTTP 服務器的套接字可以維護待處理的傳入請求的積壓。因此,如果請求在該套接字處理其它內容時進入,則它很可能只是在處理之前稍微排隊等待一會。對于低到中等流量的站點,幾分之一秒的時間并不是什么大問題,你可以使用多個部署的實例以及 NGINX 等負載均衡器來為更大的請求負載分配流量。
但是,如果你的平均響應時間超過一秒鐘,該怎么辦?如果你使用來自傳入請求的數據來啟動一些長時間的過程(如機器學習算法或某些海量數據庫查詢),該怎么辦?現在,你的單線程 Web 服務器開始累積一個無法尋址的積壓請求,其中一些請求會因為超時而被丟棄。這不是一種選擇,特別是如果你希望你的服務在一段時間內是可靠的。
異步 Python 程序登場。重要的是要記住因為它是用 Python 編寫的,所以程序仍然是一個單線程進程。除非特別標記,否則在異步程序中仍然會阻塞執(zhí)行。
但是,當異步程序結構正確時,只要你指定某個函數應該具有這樣的能力,你的異步 Python 程序就可以“擱置”長時間運行的任務。然后,當擱置的任務完成并準備好恢復時,異步控制器會收到報告,只要在需要時管理它們的執(zhí)行,而不會完全阻塞對新輸入的處理。
這有點夸張,所以讓我們用一個人類的例子來證明。
帶回家吧
我經常發(fā)現自己在家里試圖完成很多家務,但沒有多少時間來做它們。在某一天,積壓的家務可能看起來像:
* 做飯(20 分鐘準備,40 分鐘烹飪)* 洗碗(60 分鐘)* 洗滌并擦干衣物(30 分鐘洗滌,每次干燥 90 分鐘)* 真空清洗地板(30 分鐘)
如果我是一個傳統(tǒng)的同步程序,我會親自完成每項任務。在我考慮處理任何其他事情之前,每項任務都需要我全神貫注地完成。因為如果沒有我的全力關注,什么事情都完成不了。所以我的執(zhí)行順序可能如下:
1. 完全專注于準備和烹飪食物,包括等待食物烹飪(60 分鐘)2. 將臟盤子移到水槽中(65 分鐘過去了)3. 清洗所有盤子(125 分鐘過去了)4. 開始完全專注于洗衣服,包括等待洗衣機洗完,然后將衣物轉移到烘干機,再等烘干機完成( 250 分鐘過去了)5. 對地板進行真空吸塵(280 分鐘了)
從頭到尾完成所有事情花費了 4 小時 40 分鐘。
我應該像異步程序一樣聰明地工作,而不是努力工作。我的家里到處都是可以為我工作的機器,而不用我一直努力工作。同時,現在我可以將注意力轉移真正需要的東西上。
我的執(zhí)行順序可能看起來像:
1. 將衣物放入洗衣機并啟動它(5 分鐘)2. 在洗衣機運行時,準備食物(25 分鐘過去了)3. 準備好食物后,開始烹飪食物(30 分鐘過去了)4. 在烹飪食物時,將衣物從洗衣機移到烘干機機中開始烘干(35 分鐘過去了)5. 當烘干機運行中,且食物仍在烹飪時,對地板進行真空吸塵(65 分鐘過去了)6. 吸塵后,將食物從爐子中取出并裝盤子入洗碗機(70 分鐘過去了)7. 運行洗碗機(130 分鐘完成)
現在花費的時間下降到 2 小時 10 分鐘。即使我允許在作業(yè)之間切換花費更多時間(總共 10-20 分鐘)。如果我等待著按順序執(zhí)行每項任務,我花費的時間仍然只有一半左右。這就是將程序構造為異步的強大功能。
那么 I/O 循環(huán)在哪里?
一個異步 Python 程序的工作方式是從某個外部源(輸入)獲取數據,如果某個進程需要,則將該數據轉移到某個外部工作者(輸出)進行處理。當外部進程完成時,Python 主程序會收到提醒,然后程序獲取外部處理(輸入)的結果,并繼續(xù)這樣其樂融融的方式。
當數據不在 Python 主程序手中時,主程序就會被釋放來處理其它任何事情。包括等待全新的輸入(如 HTTP 請求)和處理長時間運行的進程的結果(如機器學習算法的結果,長時間運行的數據庫查詢)。主程序雖仍然是單線程的,但成了事件驅動的,它對程序處理的特定事件會觸發(fā)動作。監(jiān)聽這些事件并指示應如何處理它們的主要是 I/O 循環(huán)在工作。
我知道,我們走了很長的路才得到這個重要的解釋,但我希望在這里傳達的是,它不是魔術,也不是某種復雜的并行處理或多線程工作。全局解釋器鎖仍然存在,主程序中任何長時間運行的進程仍然會阻塞其它任何事情的進行,該程序仍然是單線程的。然而,通過將繁瑣的工作外部化,我們可以將線程的注意力集中在它需要注意的地方。
這有點像我上面的異步任務。當我的注意力完全集中在準備食物上時,它就是我所能做的一切。然而,當我能讓爐子幫我做飯,洗碗機幫我洗碗,洗衣機和烘干機幫我洗衣服時,我的注意力就會被釋放出來,去做其它事情。當我被提醒,我的一個長時間運行的任務已經完成并準備再次處理時,如果我的注意力是空閑的,我可以獲取該任務的結果,并對其做下一步需要做的任何事情。
Tornado 路由和視圖
盡管經歷了在 Python 中討論異步的所有麻煩,我們還是決定暫不使用它。先來編寫一個基本的 Tornado 視圖。
與我們在 Flask 和 Pyramid 實現中看到的基于函數的視圖不同,Tornado 的視圖都是基于類的。這意味著我們將不在使用單獨的、獨立的函數來規(guī)定如何處理請求。相反,傳入的 HTTP 請求將被捕獲并將其分配為我們定義的類的一個屬性。然后,它的方法將處理相應的請求類型。
讓我們從一個基本的視圖開始,即在屏幕上打印 “Hello, World”。我們?yōu)?Tornado 應用程序構造的每個基于類的視圖都必須繼承 tornado.web 中的 RequestHandler 對象。這將設置我們需要(但不想寫)的所有底層邏輯來接收請求,同時構造正確格式的 HTTP 響應。
from tornado.web import RequestHandlerclass HelloWorld(RequestHandler):"""Print 'Hello, world!' as the response body."""def get(self):"""Handle a GET request for saying Hello World!."""self.write("Hello, world!")
因為我們要處理 GET 請求,所以我們聲明(實際上是重寫)了 get 方法。我們提供文本或 JSON 可序列化對象,用 self.write 寫入響應體。之后,我們讓 RequestHandler 來做在發(fā)送響應之前必須完成的其它工作。
就目前而言,此視圖與 Tornado 應用程序本身并沒有實際連接。我們必須回到 __init__.py,并稍微更新 main 函數。以下是新的內容:
# __init__.pyfrom tornado.httpserver import HTTPServerfrom tornado.ioloop import IOLoopfrom tornado.options import define, optionsfrom tornado.web import Applicationfrom todo.views import HelloWorlddefine('port', default=8888, help='port to listen on')def main():"""Construct and serve the tornado application."""app = Application([('/', HelloWorld)])http_server = HTTPServer(app)http_server.listen(options.port)print('Listening on http://localhost:%i' % options.port)IOLoop.current().start()
我們做了什么
我們將 views.py 文件中的 HelloWorld 視圖導入到腳本 __init__.py 的頂部。然后我們添加了一個路由-視圖對應的列表,作為 Application 實例化的第一個參數。每當我們想要在應用程序中聲明一個路由時,它必須綁定到一個視圖。如果需要,可以對多個路由使用相同的視圖,但每個路由必須有一個視圖。
我們可以通過在 setup.py 中啟用的 serve_app 命令來運行應用程序,從而確保這一切都能正常工作。查看 http://localhost:8888/ 并看到它顯示 “Hello, world!”。
當然,在這個領域中我們還能做更多,也將做更多,但現在讓我們來討論模型吧。
連接數據庫
如果我們想要保留數據,就需要連接數據庫。與 Flask 一樣,我們將使用一個特定于框架的 SQLAchemy 變體,名為 tornado-sqlalchemy。
為什么要使用它而不是 SQLAlchemy 呢?好吧,其實 tornado-sqlalchemy 具有簡單 SQLAlchemy 的所有優(yōu)點,因此我們仍然可以使用通用的 Base 聲明模型,并使用我們習以為常的所有列數據類型和關系。除了我們已經慣常了解到的,tornado-sqlalchemy 還為其數據庫查詢功能提供了一種可訪問的異步模式,專門用于與 Tornado 現有的 I/O 循環(huán)一起工作。
我們通過將 tornado-sqlalchemy 和 psycopg2 添加到 setup.py 到所需包的列表并重新安裝包來創(chuàng)建環(huán)境。在 models.py 中,我們聲明了模型。這一步看起來與我們在 Flask 和 Pyramid 中已經看到的完全一樣,所以我將跳過全部聲明,只列出了 Task 模型的必要部分。
# 這不是完整的 models.py, 但是足夠看到不同點from tornado_sqlalchemy import declarative_baseBase = declarative_baseclass Task(Base):# 等等,因為剩下的幾乎所有的東西都一樣 ...
我們仍然需要將 tornado-sqlalchemy 連接到實際應用程序。在 __init__.py 中,我們將定義數據庫并將其集成到應用程序中。
# __init__.pyfrom tornado.httpserver import HTTPServerfrom tornado.ioloop import IOLoopfrom tornado.options import define, optionsfrom tornado.web import Applicationfrom todo.views import HelloWorld# add theseimport osfrom tornado_sqlalchemy import make_session_factorydefine('port', default=8888, help='port to listen on')factory = make_session_factory(os.environ.get('DATABASE_URL', ''))def main():"""Construct and serve the tornado application."""app = Application([('/', HelloWorld)],session_factory=factory)http_server = HTTPServer(app)http_server.listen(options.port)print('Listening on http://localhost:%i' % options.port)IOLoop.current().start()
就像我們在 Pyramid 中傳遞的會話工廠一樣,我們可以使用 make_session_factory 來接收數據庫 URL 并生成一個對象,這個對象的唯一目的是為視圖提供到數據庫的連接。然后我們將新創(chuàng)建的 factory 傳遞給 Application 對象,并使用 session_factory 關鍵字參數將它綁定到應用程序中。
最后,初始化和管理數據庫與 Flask 和 Pyramid 相同(即,單獨的 DB 管理腳本,與 Base 對象一起工作等)。它看起來很相似,所以在這里我就不介紹了。
回顧視圖
Hello,World 總是適合學習基礎知識,但我們需要一些真實的,特定應用程序的視圖。
讓我們從 info 視圖開始。
# views.pyimport jsonfrom tornado.web import RequestHandlerclass InfoView(RequestHandler):"""只允許 GET 請求"""SUPPORTED_METHODS = ["GET"]def set_default_headers(self):"""設置默認響應頭為 json 格式的"""self.set_header("Content-Type", 'application/json; charset="utf-8"')def get(self):"""列出這個 API 的路由"""routes = {'info': 'GET /api/v1','register': 'POST /api/v1/accounts','single profile detail': 'GET /api/v1/accounts/', 'edit profile': 'PUT /api/v1/accounts/', 'delete profile': 'DELETE /api/v1/accounts/', 'login': 'POST /api/v1/accounts/login','logout': 'GET /api/v1/accounts/logout',"user's tasks": 'GET /api/v1/accounts//tasks', "create task": 'POST /api/v1/accounts//tasks', "task detail": 'GET /api/v1/accounts//tasks/ ', "task update": 'PUT /api/v1/accounts//tasks/ ', "delete task": 'DELETE /api/v1/accounts//tasks/ ' }self.write(json.dumps(routes))
有什么改變嗎?讓我們從上往下看。
我們添加了 SUPPORTED_METHODS 類屬性,它是一個可迭代對象,代表這個視圖所接受的請求方法,其他任何方法都將返回一個 405 狀態(tài)碼。當我們創(chuàng)建 HelloWorld 視圖時,我們沒有指定它,主要是當時有點懶。如果沒有這個類屬性,此視圖將響應任何試圖綁定到該視圖的路由的請求。
我們聲明了 set_default_headers 方法,它設置 HTTP 響應的默認頭。我們在這里聲明它,以確保我們返回的任何響應都有一個 "Content-Type" 是 "application/json" 類型。
我們將 json.dumps(some_object) 添加到 self.write 的參數中,因為它可以很容易地構建響應主體的內容。
現在已經完成了,我們可以繼續(xù)將它連接到 __init__.py 中的主路由。
# __init__.pyfrom tornado.httpserver import HTTPServerfrom tornado.ioloop import IOLoopfrom tornado.options import define, optionsfrom tornado.web import Applicationfrom todo.views import InfoView# 添加這些import osfrom tornado_sqlalchemy import make_session_factorydefine('port', default=8888, help='port to listen on')factory = make_session_factory(os.environ.get('DATABASE_URL', ''))def main():"""Construct and serve the tornado application."""app = Application([('/', InfoView)],session_factory=factory)http_server = HTTPServer(app)http_server.listen(options.port)print('Listening on http://localhost:%i' % options.port)IOLoop.current().start()
我們知道,還需要編寫更多的視圖和路由。每個都會根據需要放入 Application 路由列表中,每個視圖還需要一個 set_default_headers 方法。在此基礎上,我們還將創(chuàng)建 send_response 方法,它的作用是將響應與我們想要給響應設置的任何自定義狀態(tài)碼打包在一起。由于每個視圖都需要這兩個方法,因此我們可以創(chuàng)建一個包含它們的基類,這樣每個視圖都可以繼承基類。這樣,我們只需要編寫一次。
# views.pyimport jsonfrom tornado.web import RequestHandlerclass BaseView(RequestHandler):"""Base view for this application."""def set_default_headers(self):"""Set the default response header to be JSON."""self.set_header("Content-Type", 'application/json; charset="utf-8"')def send_response(self, data, status=200):"""Construct and send a JSON response with appropriate status code."""self.set_status(status)self.write(json.dumps(data))
對于我們即將編寫的 TaskListView 這樣的視圖,我們還需要一個到數據庫的連接。我們需要 tornado_sqlalchemy 中的 SessionMixin 在每個視圖類中添加一個數據庫會話。我們可以將它放在 BaseView 中,這樣,默認情況下,從它繼承的每個視圖都可以訪問數據庫會話。
# views.pyimport jsonfrom tornado_sqlalchemy import SessionMixinfrom tornado.web import RequestHandlerclass BaseView(RequestHandler, SessionMixin):"""Base view for this application."""def set_default_headers(self):"""Set the default response header to be JSON."""self.set_header("Content-Type", 'application/json; charset="utf-8"')def send_response(self, data, status=200):"""Construct and send a JSON response with appropriate status code."""self.set_status(status)self.write(json.dumps(data))
只要我們修改 BaseView 對象,在將數據發(fā)布到這個 API 時,我們就應該定位到這里。
當 Tornado(從 v.4.5 開始)使用來自客戶端的數據并將其組織起來到應用程序中使用時,它會將所有傳入數據視為字節(jié)串。但是,這里的所有代碼都假設使用 Python 3,因此我們希望使用的唯一字符串是 Unicode 字符串。我們可以為這個 BaseView 類添加另一個方法,它的工作是將輸入數據轉換為 Unicode,然后再在視圖的其他地方使用。
如果我們想要在正確的視圖方法中使用它之前轉換這些數據,我們可以重寫視圖類的原生 prepare 方法。它的工作是在視圖方法運行前運行。如果我們重寫 prepare 方法,我們可以設置一些邏輯來運行,每當收到請求時,這些邏輯就會執(zhí)行字節(jié)串到 Unicode 的轉換。
# views.pyimport jsonfrom tornado_sqlalchemy import SessionMixinfrom tornado.web import RequestHandlerclass BaseView(RequestHandler, SessionMixin):"""Base view for this application."""def prepare(self):self.form_data = {key: [val.decode('utf8') for val in val_list]for key, val_list in self.request.arguments.items()}def set_default_headers(self):"""Set the default response header to be JSON."""self.set_header("Content-Type", 'application/json; charset="utf-8"')def send_response(self, data, status=200):"""Construct and send a JSON response with appropriate status code."""self.set_status(status)self.write(json.dumps(data))
如果有任何數據進入,它將在 self.request.arguments 字典中找到。我們可以通過鍵訪問該數據庫,并將其內容(始終是列表)轉換為 Unicode。因為這是基于類的視圖而不是基于函數的,所以我們可以將修改后的數據存儲為一個實例屬性,以便以后使用。我在這里稱它為 form_data,但它也可以被稱為 potato。關鍵是我們可以存儲提交給應用程序的數據。
異步視圖方法
現在我們已經構建了 BaseaView,我們可以構建 TaskListView 了,它會繼承 BaseaView。
正如你可以從章節(jié)標題中看到的那樣,以下是所有關于異步性的討論。TaskListView 將處理返回任務列表的 GET 請求和用戶給定一些表單數據來創(chuàng)建新任務的 POST 請求。讓我們首先來看看處理 GET 請求的代碼。
# all the previous importsimport datetimefrom tornado.gen import coroutinefrom tornado_sqlalchemy import as_futurefrom todo.models import Profile, Task# the BaseView is above hereclass TaskListView(BaseView):"""View for reading and adding new tasks."""SUPPORTED_METHODS = ("GET", "POST",)@coroutinedef get(self, username):"""Get all tasks for an existing user."""with self.make_session() as session:profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)if profile:tasks = [task.to_dict() for task in profile.tasks]self.send_response({'username': profile.username,'tasks': tasks})
這里的第一個主要部分是 @coroutine 裝飾器,它從 tornado.gen 導入。任何具有與調用堆棧的正常流程不同步的 Python 可調用部分實際上是“協(xié)程”,即一個可以與其它協(xié)程一起運行的協(xié)程。在我的家務勞動的例子中,幾乎所有的家務活都是一個共同的例行協(xié)程。有些阻止了例行協(xié)程(例如,給地板吸塵),但這種例行協(xié)程只會阻礙我開始或關心其它任何事情的能力。它沒有阻止已經啟動的任何其他協(xié)程繼續(xù)進行。
Tornado 提供了許多方法來構建一個利用協(xié)程的應用程序,包括允許我們設置函數調用鎖,同步異步協(xié)程的條件,以及手動修改控制 I/O 循環(huán)的事件系統(tǒng)。
這里使用 @coroutine 裝飾器的唯一條件是允許 get 方法將 SQL 查詢作為后臺進程,并在查詢完成后恢復,同時不阻止 Tornado I/O 循環(huán)去處理其他傳入的數據源。這就是關于此實現的所有“異步”:帶外數據庫查詢。顯然,如果我們想要展示異步 Web 應用程序的魔力和神奇,那么一個任務列表就不是好的展示方式。
但是,這就是我們正在構建的,所以讓我們來看看方法如何利用 @coroutine 裝飾器。SessionMixin 混合到 BaseView 聲明中,為我們的視圖類添加了兩個方便的,支持數據庫的屬性:session 和 make_session。它們的名字相似,實現的目標也相當相似。
self.session 屬性是一個關注數據庫的會話。在請求-響應周期結束時,在視圖將響應發(fā)送回客戶端之前,任何對數據庫的更改都被提交,并關閉會話。
self.make_session 是一個上下文管理器和生成器,可以動態(tài)構建和返回一個全新的會話對象。第一個 self.session 對象仍然存在。無論如何,反正 make_session 會創(chuàng)建一個新的。make_session 生成器還為其自身提供了一個功能,用于在其上下文(即縮進級別)結束時提交和關閉它創(chuàng)建的會話。
如果你查看源代碼,則賦值給 self.session 的對象類型與 self.make_session 生成的對象類型之間沒有區(qū)別,不同之處在于它們是如何被管理的。
使用 make_session 上下文管理器,生成的會話僅屬于上下文,在該上下文中開始和結束。你可以使用 make_session 上下文管理器在同一個視圖中打開,修改,提交以及關閉多個數據庫會話。
self.session 要簡單得多,當你進入視圖方法時會話已經打開,在響應被發(fā)送回客戶端之前會話就已提交。
雖然讀取文檔片段和 PyPI 示例都說明了上下文管理器的使用,但是沒有說明 self.session 對象或由 self.make_session 生成的 session 本質上是不是異步的。當我們啟動查詢時,我們開始考慮內置于 tornado-sqlalchemy 中的異步行為。
tornado-sqlalchemy 包為我們提供了 as_future 函數。它的工作是裝飾 tornado-sqlalchemy 會話構造的查詢并 yield 其返回值。如果視圖方法用 @coroutine 裝飾,那么使用 yield as_future(query) 模式將使封裝的查詢成為一個異步后臺進程。I/O 循環(huán)會接管等待查詢的返回值和 as_future 創(chuàng)建的 future 對象的解析。
要訪問 as_future(query) 的結果,你必須從它 yield。否則,你只能獲得一個未解析的生成器對象,并且無法對查詢執(zhí)行任何操作。
這個視圖方法中的其他所有內容都與之前課堂上的類似,與我們在 Flask 和 Pyramid 中看到的內容類似。
post 方法看起來非常相似。為了保持一致性,讓我們看一下 post 方法以及它如何處理用 BaseView 構造的 self.form_data。
@coroutinedef post(self, username):"""Create a new task."""with self.make_session() as session:profile = yield as_future(session.query(Profile).filter(Profile.username == username).first)if profile:due_date = self.form_data['due_date'][0]task = Task(name=self.form_data['name'][0],note=self.form_data['note'][0],creation_date=datetime.now(),due_date=datetime.strptime(due_date, '%d/%m/%Y %H:%M:%S') if due_date else None,completed=self.form_data['completed'][0],profile_id=profile.id,profile=profile)session.add(task)self.send_response({'msg': 'posted'}, status=201)
正如我所說,這是我們所期望的:
* 與我們在 get 方法中看到的查詢模式相同 * 構造一個新的 Task 對象的實例,用 form_data 的數據填充 * 添加新的 Task 對象(但不提交,因為它由上下文管理器處理!)到數據庫會話 * 將響應發(fā)送給客戶端
這樣我們就有了 Tornado web 應用程序的基礎。其他內容(例如,數據庫管理和更多完整應用程序的視圖)實際上與我們在 Flask 和 Pyramid 應用程序中看到的相同。
關于使用合適的工具完成合適的工作的一點想法
在我們繼續(xù)瀏覽這些 Web 框架時,我們開始看到它們都可以有效地處理相同的問題。對于像這樣的待辦事項列表,任何框架都可以完成這項任務。但是,有些 Web 框架比其它框架更適合某些工作,這具體取決于對你來說什么“更合適”和你的需求。
雖然 Tornado 顯然和 Pyramid 或 Flask 一樣可以處理相同工作,但將它用于這樣的應用程序實際上是一種浪費,這就像開車從家走一個街區(qū)(LCTT 譯注:這里意思應該是從家開始走一個街區(qū)只需步行即可)。是的,它可以完成“旅行”的工作,但短途旅行不是你選擇汽車而不是自行車或者使用雙腳的原因。
根據文檔,Tornado 被稱為 “Python Web 框架和異步網絡庫”。在 Python Web 框架生態(tài)系統(tǒng)中很少有人喜歡它。如果你嘗試完成的工作需要(或將從中獲益)以任何方式、形狀或形式的異步性,使用 Tornado。如果你的應用程序需要處理多個長期連接,同時又不想犧牲太多性能,選擇 Tornado。如果你的應用程序是多個應用程序,并且需要線程感知以準確處理數據,使用 Tornado。這是它最有效的地方。
用你的汽車做“汽車的事情”,使用其他交通工具做其他事情。
向前看,進行一些深度檢查
談到使用合適的工具來完成合適的工作,在選擇框架時,請記住應用程序的范圍和規(guī)模,包括現在和未來。到目前為止,我們只研究了適用于中小型 Web 應用程序的框架。本系列的下一篇也是最后一篇將介紹最受歡迎的 Python 框架之一 Django,它適用于可能會變得更大的大型應用程序。同樣,盡管它在
文章標題:PythonWeb應用程序Tornado框架簡介
瀏覽路徑:http://fisionsoft.com.cn/article/dpsoodh.html


咨詢
建站咨詢
