Tornado Python Web 应用程序框架简介

在本系列比较 Python 框架的第三部分中,了解为处理异步进程而构建的 Tornado。
284 位读者喜欢这个。
tornado photo

Internet Archive Book Images。由 Opensource.com 修改。CC BY-SA 4.0

在本系列比较不同 Python Web 框架的前两篇文章中,我们介绍了 PyramidFlask 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 分钟)

如果我像传统的同步程序一样行动,我会自己手动完成每项任务。 在我可以考虑处理其他任何事情之前,每项任务都需要我全神贯注地完成,因为如果没有我的积极关注,任何事情都无法完成。 所以我的执行顺序可能看起来像

  1. 全神贯注于准备和烹饪食物,包括等待食物只是……烹饪(60 分钟)。
  2. 将脏盘子转移到水槽(65 分钟过去)。
  3. 清洗所有盘子(125 分钟过去)。
  4. 全神贯注地开始洗衣服,包括等待洗衣机完成,然后将衣物转移到烘干机,并等待烘干机完成(250 分钟过去)。
  5. 吸尘地板(280 分钟过去)。

从头到尾完成我的家务活需要 4 小时 40 分钟。

我应该像异步程序一样聪明地工作,而不是努力工作。 我的家到处都是可以为我做工作而无需我持续努力的机器。 同时,我可以将我的注意力转移到可能现在积极需要它的东西上。

我的执行顺序可能如下所示

  1. 将衣服放入洗衣机并启动(5 分钟)。
  2. 在洗衣机运行时,准备食物(25 分钟过去)。
  3. 准备好食物后,开始烹饪食物(30 分钟过去)。
  4. 在食物烹饪时,将衣服从洗衣机移到烘干机并启动烘干机(35 分钟过去)。
  5. 在烘干机运行和食物仍在烹饪时,吸尘地板(65 分钟过去)。
  6. 吸尘地板后,将食物从炉子上取下并装入洗碗机(70 分钟过去)。
  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 响应。

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

我们做了什么?

我们从 views.py 文件中导入了 HelloWorld 视图到 __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-sqlalchemypsycopg2 添加到 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 始终是学习基础知识的好方法,但我们需要一些真正的、特定于应用程序的视图。

让我们从 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"

我们将 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_sqlalchemySessionMixin 在每个视图类中添加一个数据库会话。 我们可以将其折叠到 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 应用程序的魔力,那么 To-Do List 并不是一个好方法。

但是,这就是我们正在构建的,因此让我们看看我们的方法如何利用该 @coroutine 装饰器。 SessionMixin 已被混合到 BaseView 声明中,它向我们的视图类添加了两个方便的、了解数据库的属性:sessionmake_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 框架时,我们开始看到的是,它们都可以有效地处理相同的问题。 对于像这样的 To-Do List,任何框架都可以完成这项工作。 但是,根据 “更合适” 对您和您的需求意味着什么,某些 Web 框架比其他 Web 框架更适合某些工作。

虽然 Tornado 显然能够处理与 Pyramid 或 Flask 相同的工作,但将其用于像这样的应用程序实际上是一种浪费。 这就像使用汽车从家行驶一个街区。 是的,它可以完成 “出行” 的工作,但短途旅行并不是您选择使用汽车而不是自行车或步行的原因。

根据文档,Tornado 被称为 “Python Web 框架和异步网络库”。 在 Python Web 框架生态系统中,几乎没有类似的东西。 如果您尝试完成的工作需要异步性(或从中受益匪浅),请使用 Tornado。 如果您的应用程序需要处理多个长时间连接,同时又不会牺牲太多性能,请选择 Tornado。 如果您的应用程序是多个应用程序合而为一,并且需要具有线程意识才能准确处理数据,请使用 Tornado。 这是它工作最好的地方。

使用您的汽车做 “汽车的事情”。 使用其他交通方式来做其他一切事情。

展望未来和一点视角检查。

说到使用合适的工具来完成合适的工作,请记住在选择框架时,您应用程序的范围和规模,无论是现在还是将来。 到目前为止,我们只研究了用于中小型 Web 应用程序的框架。 本系列的下一个也是最后一个部分将介绍最流行的 Python 框架之一 Django,它适用于可能会变得更大的大型应用程序。 同样,虽然从技术上讲它可以并且将会处理 To-Do List 问题,但请记住,这实际上不是该框架的目的。 我们仍然会对其进行测试,以展示如何使用它构建应用程序,但我们必须牢记框架的意图以及如何在它的架构中体现出来。

  • Flask: 适用于小型、简单的项目; 使我们能够轻松地构建视图并将它们快速连接到路由; 可以封装在单个文件中,而无需大惊小怪。
  • Pyramid: 适用于可能增长的项目; 包含相当多的配置才能启动和运行; 应用程序组件的单独领域可以轻松地划分和构建到任意深度,而不会失去对中央应用程序的关注。
  • Tornado: 适用于受益于精确而有意的 I/O 控制的项目; 允许使用协程,并轻松公开可以控制如何接收请求/发送响应以及何时发生这些操作的方法。
  • Django: (正如我们将看到的)适用于可能变得更大的大事; 大量的插件和mod; 在其配置和管理方面非常固执己见,以保持所有不同部分的正常运行。

无论您是从本系列的第一篇文章开始阅读还是稍后加入,感谢您的阅读! 请随时留下问题或评论。 下次我将带着满满的 Django 与您见面。

向 Python BDFL 致敬

我必须给予应有的荣誉。 非常感谢 Guido van Rossum,他不仅创造了我最喜欢的编程语言。

PyCascades 2018 期间,我很幸运不仅可以进行本次文章系列所基于的演讲,还可以被邀请参加演讲者晚宴。 我整晚都坐在 Guido 旁边,并向他提出了很多问题。 其中一个问题是异步在 Python 中是如何工作的,他毫不犹豫地花时间向我解释,我开始掌握这个概念。 后来,他 在推特上给我 一个学习 Python 异步的绝佳资源,我随后在三个月内读了三遍,然后写了这篇文章。 你是一个很棒的人,Guido!

10 条评论

接受使用条款。

你好!
我收到一个错误
(tornado_todo-Q6AvqvVo) dengolius@aspire ~/tornado_todo $ serve_app
serve_app: command not found

我必须做什么才能解决问题?

serve_app 不适用于这些配置 - command not found

我运行了所有命令,正如您所写的那样,但您没有将此命令添加到您的手册中(所以不,我没有使用这些命令安装项目...

回复 nhuntwalker

如果这不清楚,我深表歉意。 我在本系列之前的文章(这是第 3 篇文章)中对此进行了介绍,因此我选择这次跳过该细节。

回复 Denis G.

很棒的博客,几乎是异步 Web API 后端的资源! 有兴趣在顶部开发一些包装器,以使事情变得容易! :D

感谢您的指南。
我只想更正 sqlalchemy-tornado 模块的指南说明。
上下文管理器不会在最后将更改提交到数据库。 我在测试我的应用程序时发现了它。
如果您希望应用更改,则无论如何都应该自己调用 session.commit()。(我认为它也应该用 as_future() 包装)
因此,我更喜欢使用作为请求-self 对象成员的会话。
(所以,不要忘记您应该在请求处理完成时手动关闭它(处理程序类的 on_finish() 方法可以帮助您))

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.