歡迎光臨
每天分享高質量文章

Python Web 應用程式 Tornado 框架簡介 | Linux 中國

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

— Nicholas Hunt-walker

 

在這個由四部分組成的系列文章的前兩篇中,我們介紹了 Pyramid[1] 和 Flask[2] Web 框架。我們已經構建了兩次相同的應用程式,看到了一個完整的 DIY 框架和包含了更多功能的框架之間的異同。

現在讓我們來看看另一個稍微不同的選擇:Tornado 框架[3]。Tornado 在很大程度上與 Flask 一樣簡單,但有一個主要區別:Tornado 是專門為處理異步行程而構建的。在我們本系列所構建的應用程式中,這種特殊的醬料(LCTT 譯註:這裡意思是 Tornado 的異步功能)在我們構建的 app 中並不是非常有用,但我們將看到在哪裡可以使用它,以及它在更一般的情況下是如何工作的。

讓我們繼續前兩篇文章中樣式,首先從處理設置和配置開始。

Tornado 啟動和配置

如果你一直關註這個系列,那麼第一步應該對你來說習以為常。

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

創建一個 setup.py 檔案來安裝我們的應用程式相關的東西:

  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. )

因為 Tornado 不需要任何外部配置,所以我們可以直接編寫 Python 代碼來讓程式運行。讓我們創建 todo 目錄,並用需要的前幾個檔案填充它。

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

就像 Flask 和 Pyramid 一樣,Tornado 也有一些基本配置,放在 __init__.py 中。從 tornado.web 中,我們將匯入 Application 物件,它將處理路由和視圖的連接,包括資料庫(當我們談到那裡時再說)以及運行 Tornado 應用程式所需的其它額外設置。

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

像 Flask 一樣,Tornado 主要是一個 DIY 框架。當構建我們的 app 時,我們必須設置該應用實體。因為 Tornado 用它自己的 HTTP 服務器來提供該應用,我們必須設置如何提供該應用。首先,在 tornado.options.define 中定義要監聽的端口。然後我們實體化 Tornado 的 HTTPServer,將該 Application 物件的實體作為引數傳遞給它。

  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)

當我們使用 define 函式時,我們最終會在 options 物件上創建屬性。第一個引數位置的任何內容都將是屬性的名稱,分配給 default 關鍵字引數的內容將是該屬性的值。

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

在 HTTPServer 上呼叫 listen 並不會啟動服務器。我們必須再做一步,找一個可以監聽請求並傳迴響應的工作應用程式,我們需要一個輸入輸出迴圈。幸運的是,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 陳述句,來告訴我什麼時候應用程式正在提供服務,這是我的習慣。如果你願意,可以不使用 print

我們以 IOLoop.current().start() 開始我們的 I/O 迴圈。讓我們進一步討論輸入,輸出和異步性。

Python 中的異步和 I/O 迴圈的基礎知識

請允許我提前說明,我絕對,肯定,一定並且放心地說不是異步編程方面的專家。就像我寫的所有內容一樣,接下來的內容源於我對這個概念的理解的局限性。因為我是人,可能有很深很深的缺陷。

異步程式的主要問題是:

  1. * 資料如何進來?
  2. * 資料如何出去?
  3. * 什麼時候可以在不占用我全部註意力情況下運行某個過程?

由於全域性解釋器鎖[4](GIL),Python 被設計為一種單執行緒[5]語言。對於 Python 程式必須執行的每個任務,其執行緒執行的全部註意力都集中在該任務的持續時間內。我們的 HTTP 服務器是用 Python 編寫的,因此,當接收到資料(如 HTTP 請求)時,服務器的唯一關心的是傳入的資料。這意味著,在大多數情況下,無論是程式需要運行還是處理資料,程式都將完全消耗服務器的執行執行緒,阻止接收其它可能的資料,直到服務器完成它需要做的事情。

在許多情況下,這不是太成問題。典型的 Web 請求,響應周期只需要幾分之一秒。除此之外,構建 HTTP 服務器的套接字可以維護待處理的傳入請求的積壓。因此,如果請求在該套接字處理其它內容時進入,則它很可能只是在處理之前稍微排隊等待一會。對於低到中等流量的站點,幾分之一秒的時間並不是什麼大問題,你可以使用多個部署的實體以及 NGINX[6] 等負載均衡器來為更大的請求負載分配流量。

但是,如果你的平均響應時間超過一秒鐘,該怎麼辦?如果你使用來自傳入請求的資料來啟動一些長時間的過程(如機器學習演算法或某些海量資料庫查詢),該怎麼辦?現在,你的單執行緒 Web 服務器開始累積一個無法尋址的積壓請求,其中一些請求會因為超時而被丟棄。這不是一種選擇,特別是如果你希望你的服務在一段時間內是可靠的。

異步 Python 程式登場。重要的是要記住因為它是用 Python 編寫的,所以程式仍然是一個單執行緒行程。除非特別標記,否則在異步程式中仍然會阻塞執行。

但是,當異步程式結構正確時,只要你指定某個函式應該具有這樣的能力,你的異步 Python 程式就可以“擱置”長時間運行的任務。然後,當擱置的任務完成並準備好恢復時,異步控制器會收到報告,只要在需要時管理它們的執行,而不會完全阻塞對新輸入的處理。

這有點誇張,所以讓我們用一個人類的例子來證明。

帶回家吧

我經常發現自己在家裡試圖完成很多家務,但沒有多少時間來做它們。在某一天,積壓的家務可能看起來像:

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

如果我是一個傳統的同步程式,我會親自完成每項任務。在我考慮處理任何其他事情之前,每項任務都需要我全神貫註地完成。因為如果沒有我的全力關註,什麼事情都完成不了。所以我的執行順序可能如下:

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

從頭到尾完成所有事情花費了 4 小時 40 分鐘。

我應該像異步程式一樣聰明地工作,而不是努力工作。我的家裡到處都是可以為我工作的機器,而不用我一直努力工作。同時,現在我可以將註意力轉移真正需要的東西上。

我的執行順序可能看起來像:

  1. 1. 將衣物放入洗衣機並啟動它(5 分鐘)
  2. 2. 在洗衣機運行時,準備食物(25 分鐘過去了)
  3. 3. 準備好食物後,開始烹飪食物(30 分鐘過去了)
  4. 4. 在烹飪食物時,將衣物從洗衣機移到烘乾機機中開始烘乾(35 分鐘過去了)
  5. 5. 當烘乾機運行中,且食物仍在烹飪時,對地板進行真空吸塵(65 分鐘過去了)
  6. 6. 吸塵後,將食物從爐子中取出並裝盤子入洗碗機(70 分鐘過去了)
  7. 7. 運行洗碗機(130 分鐘完成)

現在花費的時間下降到 2 小時 10 分鐘。即使我允許在作業之間切換花費更多時間(總共 10-20 分鐘)。如果我等待著按順序執行每項任務,我花費的時間仍然只有一半左右。這就是將程式構造為異步的強大功能。

那麼 I/O 迴圈在哪裡?

一個異步 Python 程式的工作方式是從某個外部源(輸入)獲取資料,如果某個行程需要,則將該資料轉移到某個外部工作者(輸出)進行處理。當外部行程完成時,Python 主程式會收到提醒,然後程式獲取外部處理(輸入)的結果,並繼續這樣其樂融融的方式。

當資料不在 Python 主程式手中時,主程式就會被釋放來處理其它任何事情。包括等待全新的輸入(如 HTTP 請求)和處理長時間運行的行程的結果(如機器學習演算法的結果,長時間運行的資料庫查詢)。主程式雖仍然是單執行緒的,但成了事件驅動的,它對程式處理的特定事件會觸發動作。監聽這些事件並指示應如何處理它們的主要是 I/O 迴圈在工作。

我知道,我們走了很長的路才得到這個重要的解釋,但我希望在這裡傳達的是,它不是魔術,也不是某種複雜的並行處理或多執行緒工作。全域性解釋器鎖仍然存在,主程式中任何長時間運行的行程仍然會阻塞其它任何事情的進行,該程式仍然是單執行緒的。然而,通過將繁瑣的工作外部化,我們可以將執行緒的註意力集中在它需要註意的地方。

這有點像我上面的異步任務。當我的註意力完全集中在準備食物上時,它就是我所能做的一切。然而,當我能讓爐子幫我做飯,洗碗機幫我洗碗,洗衣機和烘乾機幫我洗衣服時,我的註意力就會被釋放出來,去做其它事情。當我被提醒,我的一個長時間運行的任務已經完成並準備再次處理時,如果我的註意力是空閑的,我可以獲取該任務的結果,並對其做下一步需要做的任何事情。

Tornado 路由和視圖

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

與我們在 Flask 和 Pyramid 實現中看到的基於函式的視圖不同,Tornado 的視圖都是基於類的。這意味著我們將不在使用單獨的、獨立的函式來規定如何處理請求。相反,傳入的 HTTP 請求將被捕獲並將其分配為我們定義的類的一個屬性。然後,它的方法將處理相應的請求型別。

讓我們從一個基本的視圖開始,即在屏幕上打印 “Hello, World”。我們為 Tornado 應用程式構造的每個基於類的視圖都必須繼承 tornado.web 中的 RequestHandler 物件。這將設置我們需要(但不想寫)的所有底層邏輯來接收請求,同時構造正確格式的 HTTP 響應。

  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!")

因為我們要處理 GET 請求,所以我們宣告(實際上是重寫)了 get 方法。我們提供文本或 JSON 可序列化物件,用 self.write 寫入響應體。之後,我們讓 RequestHandler 來做在發送響應之前必須完成的其它工作。

就目前而言,此視圖與 Tornado 應用程式本身並沒有實際連接。我們必須回到 __init__.py,並稍微更新 main 函式。以下是新的內容:

  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 視圖匯入到腳本 __init__.py 的頂部。然後我們添加了一個路由-視圖對應的串列,作為 Application 實體化的第一個引數。每當我們想要在應用程式中宣告一個路由時,它必須系結到一個視圖。如果需要,可以對多個路由使用相同的視圖,但每個路由必須有一個視圖。

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

當然,在這個領域中我們還能做更多,也將做更多,但現在讓我們來討論模型吧。

連接資料庫

如果我們想要保留資料,就需要連接資料庫。與 Flask 一樣,我們將使用一個特定於框架的 SQLAchemy 變體,名為 tornado-sqlalchemy[7]

為什麼要使用它而不是 SQLAlchemy[8] 呢?好吧,其實 tornado-sqlalchemy 具有簡單 SQLAlchemy 的所有優點,因此我們仍然可以使用通用的 Base 宣告模型,並使用我們習以為常的所有列資料型別和關係。除了我們已經慣常瞭解到的,tornado-sqlalchemy 還為其資料庫查詢功能提供了一種可訪問的異步樣式,專門用於與 Tornado 現有的 I/O 迴圈一起工作。

我們通過將 tornado-sqlalchemy 和 psycopg2 添加到 setup.py 到所需包的串列並重新安裝包來創建環境。在 models.py 中,我們宣告了模型。這一步看起來與我們在 Flask 和 Pyramid 中已經看到的完全一樣,所以我將跳過全部宣告,只列出了 Task 模型的必要部分。

  1. # 這不是完整的 models.py, 但是足夠看到不同點
  2. from tornado_sqlalchemy import declarative_base
  3. Base = declarative_base
  4. class Task(Base):
  5.     # 等等,因為剩下的幾乎所有的東西都一樣 ...

我們仍然需要將 tornado-sqlalchemy 連接到實際應用程式。在 __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 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 來接收資料庫 URL 並生成一個物件,這個物件的唯一目的是為視圖提供到資料庫的連接。然後我們將新創建的 factory 傳遞給 Application 物件,並使用 session_factory 關鍵字引數將它系結到應用程式中。

最後,初始化和管理資料庫與 Flask 和 Pyramid 相同(即,單獨的 DB 管理腳本,與 Base 物件一起工作等)。它看起來很相似,所以在這裡我就不介紹了。

回顧視圖

Hello,World 總是適合學習基礎知識,但我們需要一些真實的,特定應用程式的視圖。

讓我們從 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_essay-headers(self):
  8.         """設置預設響應頭為 json 格式的"""
  9.         self.set_essay-header("Content-Type", 'application/json; charset="utf-8"')
  10.     def get(self):
  11.         """列出這個 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 類屬性,它是一個可迭代物件,代表這個視圖所接受的請求方法,其他任何方法都將傳回一個 405[9] 狀態碼。當我們創建 HelloWorld 視圖時,我們沒有指定它,主要是當時有點懶。如果沒有這個類屬性,此視圖將響應任何試圖系結到該視圖的路由的請求。

我們宣告了 set_default_essay-headers 方法,它設置 HTTP 響應的預設頭。我們在這裡宣告它,以確保我們傳回的任何響應都有一個 "Content-Type" 是 "application/json"型別。

我們將 json.dumps(some_object) 添加到 self.write 的引數中,因為它可以很容易地構建響應主體的內容。

現在已經完成了,我們可以繼續將它連接到 __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()

我們知道,還需要編寫更多的視圖和路由。每個都會根據需要放入 Application 路由串列中,每個視圖還需要一個 set_default_essay-headers 方法。在此基礎上,我們還將創建 send_response 方法,它的作用是將響應與我們想要給響應設置的任何自定義狀態碼打包在一起。由於每個視圖都需要這兩個方法,因此我們可以創建一個包含它們的基類,這樣每個視圖都可以繼承基類。這樣,我們只需要編寫一次。

  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_essay-headers(self):
  7.         """Set the default response essay-header to be JSON."""
  8.         self.set_essay-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 這樣的視圖,我們還需要一個到資料庫的連接。我們需要 tornado_sqlalchemy 中的 SessionMixin 在每個視圖類中添加一個資料庫會話。我們可以將它放在 BaseView 中,這樣,預設情況下,從它繼承的每個視圖都可以訪問資料庫會話。

  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_essay-headers(self):
  8.         """Set the default response essay-header to be JSON."""
  9.         self.set_essay-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 物件,在將資料發佈到這個 API 時,我們就應該定位到這裡。

當 Tornado(從 v.4.5 開始)使用來自客戶端的資料並將其組織起來到應用程式中使用時,它會將所有傳入資料視為位元組串。但是,這裡的所有代碼都假設使用 Python 3,因此我們希望使用的唯一字串是 Unicode 字串。我們可以為這個 BaseView 類添加另一個方法,它的工作是將輸入資料轉換為 Unicode,然後再在視圖的其他地方使用。

如果我們想要在正確的視圖方法中使用它之前轉換這些資料,我們可以重寫視圖類的原生 prepare 方法。它的工作是在視圖方法運行前運行。如果我們重寫 prepare 方法,我們可以設置一些邏輯來運行,每當收到請求時,這些邏輯就會執行位元組串到 Unicode 的轉換。

  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_essay-headers(self):
  13.         """Set the default response essay-header to be JSON."""
  14.         self.set_essay-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))

如果有任何資料進入,它將在 self.request.arguments 字典中找到。我們可以通過鍵訪問該資料庫,並將其內容(始終是串列)轉換為 Unicode。因為這是基於類的視圖而不是基於函式的,所以我們可以將修改後的資料儲存為一個實體屬性,以便以後使用。我在這裡稱它為 form_data,但它也可以被稱為 potato。關鍵是我們可以儲存提交給應用程式的資料。

異步視圖方法

現在我們已經構建了 BaseaView,我們可以構建 TaskListView 了,它會繼承 BaseaView

正如你可以從章節標題中看到的那樣,以下是所有關於異步性的討論。TaskListView 將處理傳回任務串列的 GET 請求和用戶給定一些表單資料來創建新任務的 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.                 })

這裡的第一個主要部分是 @coroutine 裝飾器,它從 tornado.gen 匯入。任何具有與呼叫堆棧的正常流程不同步的 Python 可呼叫部分實際上是“協程”,即一個可以與其它協程一起運行的協程。在我的家務勞動的例子中,幾乎所有的家務活都是一個共同的例行協程。有些阻止了例行協程(例如,給地板吸塵),但這種例行協程只會阻礙我開始或關心其它任何事情的能力。它沒有阻止已經啟動的任何其他協程繼續進行。

Tornado 提供了許多方法來構建一個利用協程的應用程式,包括允許我們設置函式呼叫鎖,同步異步協程的條件,以及手動修改控制 I/O 迴圈的事件系統。

這裡使用 @coroutine 裝飾器的唯一條件是允許 get 方法將 SQL 查詢作為後臺行程,併在查詢完成後恢復,同時不阻止 Tornado I/O 迴圈去處理其他傳入的資料源。這就是關於此實現的所有“異步”:帶外資料庫查詢。顯然,如果我們想要展示異步 Web 應用程式的魔力和神奇,那麼一個任務串列就不是好的展示方式。

但是,這就是我們正在構建的,所以讓我們來看看方法如何利用 @coroutine 裝飾器。SessionMixin 混合到 BaseView 宣告中,為我們的視圖類添加了兩個方便的,支持資料庫的屬性:session 和 make_session。它們的名字相似,實現的標的也相當相似。

self.session 屬性是一個關註資料庫的會話。在請求-響應周期結束時,在視圖將響應發送回客戶端之前,任何對資料庫的更改都被提交,並關閉會話。

self.make_session 是一個背景關係管理器和生成器,可以動態構建和傳回一個全新的會話物件。第一個 self.session 物件仍然存在。無論如何,反正 make_session 會創建一個新的。make_session 生成器還為其自身提供了一個功能,用於在其背景關係(即縮進級別)結束時提交和關閉它創建的會話。

如果你查看原始碼,則賦值給 self.session 的物件型別與 self.make_session 生成的物件型別之間沒有區別,不同之處在於它們是如何被管理的。

使用 make_session 背景關係管理器,生成的會話僅屬於背景關係,在該背景關係中開始和結束。你可以使用 make_session 背景關係管理器在同一個視圖中打開,修改,提交以及關閉多個資料庫會話。

self.session 要簡單得多,當你進入視圖方法時會話已經打開,在響應被髮送回客戶端之前會話就已提交。

雖然讀取文件片段[10]和 PyPI 示例[11]都說明瞭背景關係管理器的使用,但是沒有說明 self.session 物件或由 self.make_session 生成的 session 本質上是不是異步的。當我們啟動查詢時,我們開始考慮內置於 tornado-sqlalchemy 中的異步行為。

tornado-sqlalchemy 包為我們提供了 as_future 函式。它的工作是裝飾 tornado-sqlalchemy 會話構造的查詢並 yield 其傳回值。如果視圖方法用 @coroutine 裝飾,那麼使用 yield as_future(query) 樣式將使封裝的查詢成為一個異步後臺行程。I/O 迴圈會接管等待查詢的傳回值和 as_future 創建的 future 物件的解析。

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

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

post 方法看起來非常相似。為了保持一致性,讓我們看一下 post 方法以及它如何處理用 BaseView 構造的 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 方法中看到的查詢樣式相同   * 構造一個新的 Task 物件的實體,用 form_data 的資料填充   * 添加新的 Task 物件(但不提交,因為它由背景關係管理器處理!)到資料庫會話   * 將響應發送給客戶端

這樣我們就有了 Tornado web 應用程式的基礎。其他內容(例如,資料庫管理和更多完整應用程式的視圖)實際上與我們在 Flask 和 Pyramid 應用程式中看到的相同。

關於使用合適的工具完成合適的工作的一點想法

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

雖然 Tornado 顯然和 Pyramid 或 Flask 一樣可以處理相同工作,但將它用於這樣的應用程式實際上是一種浪費,這就像開車從家走一個街區(LCTT 譯註:這裡意思應該是從家開始走一個街區只需步行即可)。是的,它可以完成“旅行”的工作,但短途旅行不是你選擇汽車而不是自行車或者使用雙腳的原因。

根據文件,Tornado 被稱為 “Python Web 框架和異步網絡庫”。在 Python Web 框架生態系統中很少有人喜歡它。如果你嘗試完成的工作需要(或將從中獲益)以任何方式、形狀或形式的異步性,使用 Tornado。如果你的應用程式需要處理多個長期連接,同時又不想犧牲太多性能,選擇 Tornado。如果你的應用程式是多個應用程式,並且需要執行緒感知以準確處理資料,使用 Tornado。這是它最有效的地方。

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

向前看,進行一些深度檢查

談到使用合適的工具來完成合適的工作,在選擇框架時,請記住應用程式的範圍和規模,包括現在和未來。到目前為止,我們只研究了適用於中小型 Web 應用程式的框架。本系列的下一篇也是最後一篇將介紹最受歡迎的 Python 框架之一 Django,它適用於可能會變得更大的大型應用程式。同樣,儘管它在技術上能夠並且將會處理待辦事項串列問題,但請記住,這不是它的真正用途。我們仍然會通過它來展示如何使用它來構建應用程式,但我們必須牢記框架的意圖以及它是如何反映在架構中的:

◈ Flask: 適用於小型,簡單的專案。它可以使我們輕鬆地構建視圖並將它們快速連接到路由,它可以簡單地封裝在一個檔案中。 
◈ Pyramid: 適用於可能增長的專案。它包含一些配置來啟動和運行。應用程式組件的獨立領域可以很容易地劃分並構建到任意深度,而不會忽略中央應用程式。
◈ Tornado: 適用於受益於精確和有意識的 I/O 控制的專案。它允許協程,並輕鬆公開可以控制如何接收請求或發送響應以及何時發生這些操作的方法。
◈ Django:(我們將會看到)意味著可能會變得更大的東西。它有著非常龐大的生態系統,包括大量插件和模塊。它非常有主見的配置和管理,以保持所有不同部分在同一條線上。

無論你是從本系列的第一篇文章開始閱讀,還是稍後才加入的,都要感謝閱讀!請隨意留下問題或意見。下次再見時,我手裡會拿著 Django。

感謝 Python BDFL

我必須把功勞歸於它應得的地方,非常感謝 Guido van Rossum[12],不僅僅是因為他創造了我最喜歡的編程語言。

在 PyCascades 2018[13] 期間,我很幸運的不僅做了基於這個文章系列的演講,而且還被邀請參加了演講者的晚宴。整個晚上我都坐在 Guido 旁邊,不停地問他問題。其中一個問題是,在 Python 中異步到底是如何工作的,但他沒有一點大驚小怪,而是花時間向我解釋,讓我開始理解這個概念。他後來推特給我[14]發了一條訊息:是用於學習異步 Python 的廣闊資源。我隨後在三個月內閱讀了三次,然後寫了這篇文章。你真是一個非常棒的人,Guido!

赞(0)

分享創造快樂