在本系列比较不同 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
方法。我们不返回任何内容,而是提供文本或 JSON 可序列化对象,以使用 self.write
写入响应正文。之后,我们让 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
中的必需包列表并重新安装该包来设置阶段。在 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 并生成一个对象,其唯一目的是为我们的视图提供与数据库的连接。然后,我们通过使用 session_factory
关键字参数将新创建的 factory
传递到 Application
对象中,将其与我们的应用程序绑定。
最后,初始化和管理数据库看起来与 Flask 和 Pyramid 的方式相同(即,单独的 DB 管理脚本,相对于 Base
对象工作等)。它看起来太相似了,我不会在这里重现它。
重新审视视图
Hello, World 对于学习基础知识总是很好,但我们需要一些真正的、特定于应用程序的视图。
让我们从信息视图开始。
# 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"
。
我们将 json.dumps(some_object)
添加到 self.write
的参数中,因为它使构建传出响应正文的内容变得容易。
现在完成了,我们可以继续将其连接到 __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
})
这里第一个主要部分是从 tornado.gen
导入的 @coroutine
装饰器。任何具有一部分与调用堆栈的正常流程不同步的 Python 可调用对象实际上都是“协程”;可以与其他例程并行运行的例程。在我的家务示例中,几乎每项家务都是一个协程。有些是阻塞例程(例如,吸尘地板),但该例程只是阻止我开始或处理其他任何事情的能力。它没有阻止任何已经启动的其他例程继续进行。
Tornado 提供了许多构建利用协程的应用程序的方法,包括允许我们在函数调用上设置锁、同步异步例程的条件以及手动修改控制 I/O 循环的事件的系统。
此处使用 @coroutine
装饰器的唯一方法是允许 get
方法将 SQL 查询作为后台进程外包,并在查询完成后恢复,同时不阻止 Tornado I/O 循环处理其他传入数据源。这就是此实现中“异步”的全部内容:带外数据库查询。显然,如果我们想展示异步 Web 应用程序的魔力和奇迹,待办事项列表并不是正确的方法。
但是,嘿,这就是我们要构建的,所以让我们看看我们的方法如何利用 @coroutine
装饰器。已混入 BaseView
声明的 SessionMixin
为我们的视图类添加了两个方便的、数据库感知的属性: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
简单得多,在您到达视图方法时会话已打开,并在将响应发送回客户端之前提交。
尽管 文档片段 和 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 中的 async 异步是如何工作的,他毫不费力地花时间向我解释,让我开始掌握这个概念。他后来在 Twitter 上给我发了一条推文,推荐了一个学习 Python 异步的绝佳资源,我随后在三个月内读了三遍,然后写了这篇文章。Guido,你真是个了不起的人!
10 条评论