在本系列四部分文章的前两篇中,我们比较了不同的 Python web 框架,我们介绍了 Pyramid 和 Flask web 框架。我们构建了两次相同的应用程序,并看到了一个完整的 DIY 框架和一个包含更多内置功能的框架之间的异同。
现在让我们看看一个略有不同的选项:Tornado 框架。在大多数情况下,Tornado 与 Flask 一样精简,但有一个主要区别:Tornado 专门为处理异步进程而构建。这种特殊的“秘诀”在我们本系列中构建的应用程序中不是非常有用,但我们将看到在哪里可以使用它以及它在更一般的情况下的工作原理。
让我们继续我们在前两篇文章中设定的模式,首先解决设置和配置问题。
Tornado 启动和配置
如果您一直在关注本系列,那么我们首先要做的事情应该不会让您感到惊讶。
$ mkdir tornado_todo
$ cd tornado_todo
$ pipenv install --python 3.6
$ pipenv shell
(tornado-someHash) $ pipenv install tornado
创建一个 setup.py
用于安装我们的应用程序
(tornado-someHash) $ touch setup.py
# setup.py
from setuptools import setup, find_packages
requires = [
'tornado',
'tornado-sqlalchemy',
'psycopg2',
]
setup(
name='tornado_todo',
version='0.0',
description='A To-Do List built with Tornado',
author='<Your name>',
author_email='<Your email>',
keywords='web tornado',
packages=find_packages(),
install_requires=requires,
entry_points={
'console_scripts': [
'serve_app = todo:main',
],
},
)
由于 Tornado 不需要任何外部配置,我们可以直接深入编写将运行我们应用程序的 Python 代码。让我们创建内部 todo
目录,并用我们需要的前几个文件填充它。
todo/
__init__.py
models.py
views.py
与 Flask 和 Pyramid 一样,Tornado 也有一些中心配置,这些配置将放在 __init__.py
中。从 tornado.web
中,我们将导入 Application
对象。这将处理路由和视图的挂钩,包括我们的数据库(当我们到达那里时)以及运行我们的 Tornado 应用程序所需的任何额外设置。
# __init__.py
from tornado.web import Application
def main():
"""Construct and serve the tornado application."""
app = Application()
与 Flask 类似,Tornado 是一个主要由 DIY 完成的框架。在构建我们的应用程序时,我们必须设置应用程序实例。由于 Tornado 使用自己的 HTTP 服务器为应用程序提供服务,因此我们还必须设置应用程序的提供方式。首先,我们使用 tornado.options.define
定义要监听的端口。然后我们实例化 Tornado 的 HTTPServer
,并将 Application
对象的实例作为其参数传递。
# __init__.py
from tornado.httpserver import HTTPServer
from tornado.options import define, options
from tornado.web import Application
define('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
对象上创建属性。作为第一个参数的位置的任何内容都将是属性的名称,而分配给 default
关键字参数的内容将是该属性的值。
例如,如果我们将属性命名为 potato
而不是 port
,我们可以通过 options.potato
访问其值。
在 HTTPServer
上调用 listen
尚不会启动服务器。我们必须再执行一个步骤才能拥有一个可以监听请求并返回响应的工作应用程序。我们需要一个输入输出循环。值得庆幸的是,Tornado 开箱即用地提供了 tornado.ioloop.IOLoop
形式。
# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
define('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 循环。让我们更多地谈谈输入、输出和异步性。
Python 中异步和 I/O 循环的基础知识
请允许我首先声明,我绝对、肯定、确信和安全地不是异步编程方面的专家。与我写的所有东西一样,以下内容源于我对该概念理解的局限性。由于我是人,因此它可能存在严重、严重的缺陷。
异步程序的主要关注点是
- 数据如何进入?
- 数据如何流出?
- 何时可以让某些过程在不占用我全部注意力的情况下运行?
由于 全局解释器锁 (GIL),Python 在设计上是一种 单线程 语言。对于 Python 程序必须执行的每个任务,其执行线程的全部注意力都集中在该任务上,直到该任务完成。我们的 HTTP 服务器是用 Python 编写的。因此,当接收到数据(例如,HTTP 请求)时,服务器的唯一焦点是传入的数据。这意味着,在大多数情况下,处理和处理该数据中需要运行的任何过程都将完全占用服务器的执行线程,从而阻止接收其他潜在数据,直到服务器完成它需要做的任何事情。
在许多情况下,这并不是太大的问题;典型的 web 请求-响应周期只需几分之一秒。此外,构建 HTTP 服务器的套接字可以维护要处理的传入请求的积压。因此,如果在一个套接字正在处理其他事情时传入一个请求,则它很可能只是排队等待一会儿才被处理。对于低到中等流量的站点,几分之一秒并不是什么大问题,您可以使用多个部署的实例以及像 NGINX 这样的负载均衡器来分配较大请求负载的流量。
但是,如果您的平均响应时间超过几分之一秒怎么办?如果您使用来自传入请求的数据来启动一些长时间运行的进程,例如机器学习算法或一些大型数据库查询怎么办?现在,您的单线程 web 服务器开始累积无法处理的请求积压,其中一些请求将因超时而丢失。这不是一个选项,特别是如果您希望您的服务被视为定期可靠的服务。
异步 Python 程序应运而生。重要的是要记住,因为它是在 Python 中编写的,所以该程序仍然是一个单线程进程。任何会在同步程序中阻止执行的操作,除非特别标记,否则仍会在异步程序中阻止执行。
但是,当结构正确时,您的异步 Python 程序可以在您指定某个函数应具有这样做的能力时“搁置”长时间运行的任务。然后,当搁置的任务完成并准备好恢复时,您的异步控制器可以收到警报,仅在需要时才管理它们的执行,而不会完全阻止处理新输入。
这有点术语化,所以让我们用一个人类的例子来演示。
带回家
我经常发现自己试图在很短的时间内完成多项家务。在特定的一天,家务积压可能看起来像
- 做饭(准备 20 分钟,烹饪 40 分钟)
- 洗碗(60 分钟)
- 洗涤和烘干衣物(每次洗涤 30 分钟,烘干 90 分钟)
- 吸尘地板(30 分钟)
如果我像传统的同步程序一样运行,我将自己手动完成每项任务。每项任务都需要我全神贯注才能完成,然后我才能考虑处理其他任何事情,因为没有我的积极关注,任何事情都无法完成。因此,我的执行顺序可能如下所示
- 全神贯注于准备和烹饪食物,包括等待食物只是……烹饪(60 分钟)。
- 将脏盘子转移到水槽(已过去 65 分钟)。
- 清洗所有盘子(已过去 125 分钟)。
- 开始洗衣,全神贯注于此,包括等待洗衣机完成,然后将衣物转移到烘干机,并等待烘干机完成(已过去 250 分钟)。
- 吸尘地板(已过去 280 分钟)。
从头到尾完成我的家务需要 4 小时 40 分钟。
我不应该努力工作,而应该像异步程序一样聪明地工作。我的家里到处都是机器,可以在我不需要持续努力的情况下为我工作。同时,我可以将注意力转移到可能现在积极需要注意的事情上。
我的执行顺序可能改为如下所示
- 将衣服装入洗衣机并启动洗衣机(5 分钟)。
- 在洗衣机运行时,准备食物(已过去 25 分钟)。
- 准备好食物后,开始烹饪食物(已过去 30 分钟)。
- 在食物烹饪时,将衣服从洗衣机移到烘干机并启动烘干机(已过去 35 分钟)。
- 在烘干机运行且食物仍在烹饪时,吸尘地板(已过去 65 分钟)。
- 吸尘地板后,将食物从炉子上移开并装入洗碗机(已过去 70 分钟)。
- 运行洗碗机(完成时 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 响应。
from tornado.web import RequestHandler
class 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
方法。我们不返回任何内容,而是提供要使用 self.write
写入响应正文的文本或 JSON 可序列化对象。之后,我们让 RequestHandler
接管在发送响应之前必须完成的其余工作。
就目前而言,此视图与 Tornado 应用程序本身没有任何实际连接。我们必须返回到 __init__.py
并稍微更新 main
函数。这是新的热门内容
# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import HelloWorld
define('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()
我们做了什么?
我们在脚本顶部将 HelloWorld
视图从 views.py
文件导入到 __init__.py
中。然后,我们添加了一系列路由-视图对,作为实例化 Application
的第一个参数。每当我们想在我们的应用程序中声明路由时,它必须绑定到一个视图。如果您愿意,可以为多个路由使用相同的视图,但每个路由都必须始终有一个视图。
我们可以通过使用我们在 setup.py
中启用的 serve_app
命令运行我们的应用程序来确保这一切都有效。检查 http://localhost:8888/
并查看它是否显示“Hello, world!”
当然,在这个领域我们可以而且将会做更多的事情,但让我们继续讨论模型。
连接数据库
如果我们想保存数据,我们需要连接数据库。与 Flask 一样,我们将使用 SQLAlchemy 的框架特定变体,称为 tornado-sqlalchemy。
为什么要使用它而不是直接使用裸的 SQLAlchemy 呢?嗯,tornado-sqlalchemy
拥有直接使用 SQLAlchemy 的所有优点,因此我们仍然可以使用通用的 Base
声明模型,并使用我们已经习惯的所有列数据类型和关系。除了我们已经从习惯中了解到的之外,tornado-sqlalchemy
还为其数据库查询功能提供了一种可访问的异步模式,专门用于与 Tornado 现有的 I/O 循环协同工作。
我们通过将 tornado-sqlalchemy
和 psycopg2
添加到 setup.py
的 required packages 列表中并重新安装包来开始准备工作。在 models.py
中,我们声明我们的模型。这一步看起来和我们在 Flask 和 Pyramid 中已经看到的非常相似,所以我将跳过完整的类声明,只展示 Task
模型的必要部分。
# this is not the complete models.py, but enough to see the differences
from tornado_sqlalchemy import declarative_base
Base = declarative_base
class Task(Base):
# and so on, because literally everything's the same...
我们仍然需要将 tornado-sqlalchemy
连接到实际的应用程序。在 __init__.py
中,我们将定义数据库并将其集成到应用程序中。
# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import HelloWorld
# add these
import os
from tornado_sqlalchemy import make_session_factory
define('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 并生成一个对象,该对象的唯一目的是为我们的视图提供数据库连接。然后,我们通过将新创建的 factory
以 session_factory
关键字参数传递给 Application
对象,将其绑定到我们的应用程序中。
最后,初始化和管理数据库看起来将与 Flask 和 Pyramid 中的情况相同(即,单独的数据库管理脚本,与 Base
对象一起工作等等)。它看起来非常相似,以至于我不会在这里重复它。
重新审视视图
对于学习基础知识来说,Hello, World 总是很不错的,但我们需要一些真正的、特定于应用程序的视图。
让我们从 info 视图开始。
# views.py
import json
from tornado.web import RequestHandler
class InfoView(RequestHandler):
"""Only allow GET requests."""
SUPPORTED_METHODS = ["GET"]
def set_default_headers(self):
"""Set the default response header to be JSON."""
self.set_header("Content-Type", 'application/json; charset="utf-8"')
def get(self):
"""List of routes for this API."""
routes = {
'info': 'GET /api/v1',
'register': 'POST /api/v1/accounts',
'single profile detail': 'GET /api/v1/accounts/<username>',
'edit profile': 'PUT /api/v1/accounts/<username>',
'delete profile': 'DELETE /api/v1/accounts/<username>',
'login': 'POST /api/v1/accounts/login',
'logout': 'GET /api/v1/accounts/logout',
"user's tasks": 'GET /api/v1/accounts/<username>/tasks',
"create task": 'POST /api/v1/accounts/<username>/tasks',
"task detail": 'GET /api/v1/accounts/<username>/tasks/<id>',
"task update": 'PUT /api/v1/accounts/<username>/tasks/<id>',
"delete task": 'DELETE /api/v1/accounts/<username>/tasks/<id>'
}
self.write(json.dumps(routes))
那么改变了什么呢?让我们从上到下看看。
添加了 SUPPORTED_METHODS
类属性。这将是一个仅包含此视图接受的请求方法的迭代器。任何其他方法都将返回 405 状态码。当我们创建 HelloWorld
视图时,我们没有指定这一点,主要是出于懒惰。如果没有这个类属性,这个视图将响应任何尝试访问绑定到该视图的路由的请求。
声明了 set_default_headers
方法,它设置了传出 HTTP 响应的默认标头。我们在这里声明它以确保我们发送回的任何响应都具有 "Content-Type"
为 "application/json"
。
我们在 self.write
的参数中添加了 json.dumps(some_object)
,因为它使构建传出响应正文的内容变得容易。
现在完成了,我们可以继续将其连接到 __init__.py
中的主页路由。
# __init__.py
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
from tornado.web import Application
from todo.views import InfoView
# add these
import os
from tornado_sqlalchemy import make_session_factory
define('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
方法。最重要的是,我们将创建我们的 send_response
方法,其工作是将我们的响应与我们想要为给定响应设置的任何自定义状态代码一起打包。由于每个视图都需要这两种方法,我们可以创建一个包含它们的基础类,以便我们的每个视图都可以从中继承。这样,我们只需要编写一次。
# views.py
import json
from tornado.web import RequestHandler
class 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.py
import json
from tornado_sqlalchemy import SessionMixin
from tornado.web import RequestHandler
class 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
对象,我们就应该解决一个在我们考虑将数据发布到此 API 时会出现的怪癖。
当 Tornado(截至 v.4.5)从客户端接收数据并组织它以在应用程序中使用时,它会将所有传入数据保留为字节串。但是,这里的所有代码都假定使用 Python 3,因此我们想要使用的唯一字符串是 Unicode 字符串。我们可以在这个 BaseView
类中添加另一个方法,其工作是在视图中的任何其他地方使用传入数据之前将其转换为 Unicode。
如果我们想在适当的视图方法中使用此数据之前对其进行转换,我们可以覆盖视图类的原生 prepare
方法。它的工作是在视图方法运行之前运行。如果我们覆盖 prepare
方法,我们可以设置一些逻辑来运行,以便在接收到请求时执行字节串到 Unicode 的转换。
# views.py
import json
from tornado_sqlalchemy import SessionMixin
from tornado.web import RequestHandler
class 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
。重点是我们可以存储已提交给应用程序的数据。
异步视图方法
现在我们已经构建了我们的 BaseView
,我们可以构建将从它继承的 TaskListView
。
您可能从节标题中看出,这就是所有关于异步性的讨论的由来。TaskListView
将处理 GET
请求以返回任务列表,以及 POST
请求以根据一些表单数据创建新任务。让我们首先看看处理 GET
请求的代码。
# all the previous imports
import datetime
from tornado.gen import coroutine
from tornado_sqlalchemy import as_future
from todo.models import Profile, Task
# the BaseView is above here
class TaskListView(BaseView):
"""View for reading and adding new tasks."""
SUPPORTED_METHODS = ("GET", "POST",)
@coroutine
def 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 可调用对象,其一部分与调用堆栈的正常流程不同步,实际上都是一个“协程”;一个可以与其他例程并行运行的例程。在我家务的例子中,几乎每项家务都是一个协程。有些是阻塞例程(例如,吸尘地板),但该例程只是阻止了我开始或处理任何其他事情的能力。它并没有阻止任何已经开始的其他例程继续进行。
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
更简单,在您进入视图方法时会话已打开,并在响应发送回客户端之前提交。
虽然 read the docs 代码片段 和 PyPI 示例 都指定使用上下文管理器,但 self.session
对象或 self.make_session
生成的 session
都没有固有的异步性。当我们启动查询时,我们才开始考虑内置于 tornado-sqlalchemy
中的异步行为。
tornado-sqlalchemy
包为我们提供了 as_future
函数。as_future
的工作是包装由 tornado-sqlalchemy
会话构建的查询并产生其返回值。如果视图方法用 @coroutine
装饰,那么使用这个 yield as_future(query)
模式现在将使您的包装查询成为异步后台进程。I/O 循环接管,等待查询的返回值和 as_future
创建的 future
对象的解析。
要访问 as_future(query)
的结果,您必须从中 yield
。否则,您只会得到一个未解析的生成器对象,并且无法对查询执行任何操作。
此视图方法中的其他所有内容几乎都是常规操作,与我们在 Flask 和 Pyramid 中已经看到的相呼应。
post
方法看起来会非常相似。为了保持一致性,让我们看看 post
方法的外观以及它如何处理使用 BaseView
构建的 self.form_data
。
@coroutine
def 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
对象添加到数据库会话(但不提交,因为它由上下文管理器处理!) - 将响应发送回客户端
因此,我们有了 Tornado Web 应用程序的基础。其他一切(例如,数据库管理和更多视图以构建更完整的应用程序)实际上与我们在 Flask 和 Pyramid 应用程序中已经看到的相同。
关于为正确的工作使用正确的工具的思考
当我们继续研究这些 Web 框架时,我们开始看到它们都可以有效地处理相同的问题。对于像这样的待办事项列表,任何框架都可以完成这项工作。但是,根据“更合适”对您和您的需求意味着什么,某些 Web 框架比其他框架更适合某些工作。
虽然 Tornado 显然能够处理 Pyramid 或 Flask 可以处理的相同工作,但将其用于像这样的应用程序实际上是一种浪费。这就像使用汽车从家行驶一个街区。是的,它可以完成“旅行”的工作,但是短途旅行并不是您选择使用汽车而不是自行车或仅仅是步行的原因。
根据文档,Tornado 被誉为“Python Web 框架和异步网络库”。在 Python Web 框架生态系统中,像它这样的框架很少。如果您尝试完成的工作需要(或将从)任何形式的异步性中显着受益,请使用 Tornado。如果您的应用程序需要处理多个长期存在的连接,同时又不牺牲太多性能,请选择 Tornado。如果您的应用程序是多合一的应用程序,并且需要线程感知以准确处理数据,请选择 Tornado。那是它工作最佳的地方。
使用您的汽车做“汽车的事情”。使用其他交通方式做其他一切。
展望未来和一点视角检查
说到为正确的工作使用正确的工具,在选择框架时,请记住应用程序的范围和规模,无论是当前还是未来。到目前为止,我们只研究了用于中小型 Web 应用程序的框架。本系列的下一篇也是最后一篇将介绍最流行的 Python 框架之一 Django,它适用于可能变得更大的大型应用程序。同样,虽然从技术上讲它可以并且将处理待办事项列表问题,但请记住,这并不是该框架的真正用途。我们仍然会全力以赴地展示如何使用它构建应用程序,但我们必须牢记框架的意图以及这如何在架构中反映出来
- Flask: 专为小型、简单项目而设计;使我们能够轻松地构建视图并快速将它们连接到路由;可以封装在单个文件中,而无需过多麻烦
- Pyramid: 专为可能增长的项目而设计;包含相当多的配置才能启动并运行;应用程序组件的独立领域可以轻松地划分和构建到任意深度,而不会忽略中心应用程序
- Tornado: 专为受益于精确和有意的 I/O 控制的项目而设计;允许协程并轻松公开可以控制如何接收请求/发送响应以及这些操作何时发生的方法
- Django: (正如我们将看到的) 专为可能变得更大的大型事物而设计;庞大的附加组件和模组生态系统;在其配置和管理中非常固执己见,以保持所有不同的部分井井有条
无论您是从本系列的第一篇文章开始阅读,还是稍后加入,感谢您的阅读!请随时留下问题或评论。下次再见,届时我将满怀 Django 的技巧。
向 Python BDFL 致以崇高的敬意
我必须给予应有的肯定。非常感谢 Guido van Rossum,不仅仅是因为他创造了我最喜欢的编程语言。
在 PyCascades 2018 期间,我很幸运不仅可以发表本系列文章所依据的演讲,而且还被邀请参加演讲者晚宴。我整晚都坐在 Guido 旁边,向他提出问题。其中一个问题是 Python 中的异步是如何工作的,他毫不费力地花时间以我能够开始掌握概念的方式向我解释。他后来 在 Twitter 上给我发了一个推文,提供了一个学习 Python 异步的绝佳资源,我随后在三个月内读了三遍,然后写了这篇文章。您真是个了不起的人,Guido!
10 条评论