Flask Python Web 应用程序框架简介

在比较 Python 框架的系列文章的第一部分中,了解 Flask。
412 位读者喜欢这篇文章。
Hands on a keyboard with a Python book

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

如果您正在使用 Python 开发 Web 应用程序,那么您很可能正在使用框架。框架 “是一个代码库,通过为常见操作提供可重用的代码或扩展,使开发人员在构建可靠、可扩展和可维护的 Web 应用程序时更轻松”。Python 有许多框架,包括 FlaskTornadoPyramidDjango。新的 Python 开发人员经常问:我应该使用哪个框架?

本系列旨在通过比较这四个框架来帮助开发人员回答这个问题。为了比较它们的功能和操作,我将通过构建一个简单的待办事项列表 Web 应用程序的 API 的过程来介绍每个框架。API 本身相当简单

  • 网站的新访问者应该能够注册新帐户。
  • 注册用户可以登录、注销、查看他们的个人资料信息以及编辑他们的信息。
  • 注册用户可以创建新的任务项、查看他们现有的任务以及编辑现有任务。

所有这些构成了一组紧凑的 API 端点,每个后端都必须实现,以及允许的 HTTP 方法

  • GET /
  • POST /accounts
  • POST /accounts/login
  • GET /accounts/logout
  • GET, PUT, DELETE /accounts/<str : username>
  • GET, POST /accounts/<str : username>/tasks
  • GET, PUT, DELETE /accounts/<str : username>/tasks/<int : id>

每个框架都有不同的方式来组合其路由、模型、视图、数据库交互和整体应用程序配置。我将在本系列中描述每个框架的这些方面,本系列将从 Flask 开始。

Flask 启动和配置

像大多数广泛使用的 Python 库一样,Flask 包可以从 Python 包索引 (PPI) 安装。首先创建一个工作目录(类似 flask_todo 的目录名就很好),然后安装 flask 包。您还需要安装 flask-sqlalchemy,以便您的 Flask 应用程序有一种简单的方式与 SQL 数据库通信。

我喜欢在 Python 3 虚拟环境中进行此类工作。要到达那里,请在命令行输入以下内容

$ mkdir flask_todo
$ cd flask_todo
$ pipenv install --python 3.6
$ pipenv shell
(flask-someHash) $ pipenv install flask flask-sqlalchemy

如果您想将其转换为 Git 存储库,那么这是一个运行 git init 的好地方。它将成为项目的根目录,如果您想将代码库导出到不同的机器,那么在这里拥有所有必要的设置文件将有所帮助。

启动的好方法是将代码库转换为可安装的 Python 发行版。在项目的根目录,创建 setup.py 和一个名为 todo 的目录来保存源代码。

setup.py 应该看起来像这样

from setuptools import setup, find_packages

requires = [
    'flask',
    'flask-sqlalchemy',
    'psycopg2',
]

setup(
    name='flask_todo',
    version='0.0',
    description='A To-Do List built with Flask',
    author='<Your actual name here>',
    author_email='<Your actual e-mail address here>',
    keywords='web flask',
    packages=find_packages(),
    include_package_data=True,
    install_requires=requires
)

这样,无论何时您想安装或部署您的项目,您都将在 requires 列表中拥有所有必要的包。您还将拥有在 site-packages 中设置和安装包所需的一切。有关如何编写可安装的 Python 发行版的更多信息,请查看 setup.py 的文档

在包含源代码的 todo 目录中,创建一个 app.py 文件和一个空白的 __init__.py 文件。__init__.py 文件允许您像从已安装的包一样从 todo 导入。app.py 文件将是应用程序的根目录。这是所有 Flask 应用程序的优点所在,您将创建一个环境变量指向该文件。如果您正在使用 pipenv(像我一样),您可以使用 pipenv --venv 找到您的虚拟环境,并在您环境的 activate 脚本中设置该环境变量。

# in your activate script, probably at the bottom (but anywhere will do)

export FLASK_APP=$VIRTUAL_ENV/../todo/app.py
export DEBUG='True'

当您安装 Flask 时,您还安装了 flask 命令行脚本。键入 flask run 将提示虚拟环境的 Flask 包使用 FLASK_APP 环境变量指向的任何脚本中的 app 对象运行 HTTP 服务器。上面的脚本还包括一个名为 DEBUG 的环境变量,稍后会用到它。

让我们来谈谈这个 app 对象。

todo/app.py 中,您将创建一个 app 对象,它是 Flask 对象的实例。它将充当整个应用程序的中心配置对象。它用于设置扩展功能所需的应用程序组件,例如,数据库连接和身份验证帮助。

它通常用于设置将成为应用程序交互点的路由。为了解释这意味着什么,让我们看看它对应的代码。

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    """Print 'Hello, world!' as the response body."""
    return 'Hello, world!'

这是最基本的完整 Flask 应用程序。appFlask 的实例,接收脚本文件的 __name__。这让 Python 知道如何从相对于该文件的文件导入。app.route 装饰器装饰第一个视图函数;它可以指定用于访问应用程序的路由之一。(我们稍后会介绍这一点。)

您指定的任何视图都必须由 app.route 装饰才能成为应用程序的功能部分。您可以根据需要在应用程序中分散任意数量的函数,但是为了使该功能可以从应用程序外部的任何内容访问,您必须装饰该函数并指定一个路由以使其成为视图。

在上面的示例中,当应用程序正在运行并在 http://domainname/ 访问时,用户将收到 "Hello, World!" 作为响应。

在 Flask 中连接数据库

虽然上面的代码示例代表了一个完整的 Flask 应用程序,但它并没有做任何有趣的事情。Web 应用程序可以做的一件有趣的事情是持久化用户数据,但这需要数据库的帮助和连接。

Flask 在很大程度上是一个“自己动手”的 Web 框架。这意味着没有内置的数据库交互,但是 flask-sqlalchemy 包会将 SQL 数据库连接到 Flask 应用程序。flask-sqlalchemy 包只需要一件事来连接到 SQL 数据库:数据库 URL。

请注意,各种 SQL 数据库管理系统都可以与 flask-sqlalchemy 一起使用,只要 DBMS 有一个遵循 DBAPI-2 标准 的中介。在本示例中,我将使用 PostgreSQL(主要是因为我经常使用它),因此与 Postgres 数据库通信的中介是 psycopg2 包。确保 psycopg2 已安装在您的环境中,并将其包含在 setup.py 中所需的包列表中。您不必对其进行任何其他操作;flask-sqlalchemy 将从数据库 URL 中识别 Postgres。

Flask 需要数据库 URL 通过 SQLALCHEMY_DATABASE_URI 属性成为其中心配置的一部分。一个快速而粗略的解决方案是将数据库 URL 硬编码到应用程序中。

# top of app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://localhost:5432/flask_todo'
db = SQLAlchemy(app)

但是,这不是一个可持续的解决方案。如果您更改数据库或不想在源代码控制中看到您的数据库 URL,您将需要采取额外的步骤来确保您的信息适合环境。

您可以使用环境变量使事情更简单。它们将确保,无论代码在哪个机器上运行,如果这些东西在运行环境中配置,它总是指向正确的东西。它还确保,即使您需要该信息才能运行应用程序,它也永远不会在源代码控制中显示为硬编码值。

在您声明 FLASK_APP 的相同位置,声明一个 DATABASE_URL 指向您的 Postgres 数据库的位置。开发通常在本地工作,因此只需指向您的本地数据库即可。

# also in your activate script

export DATABASE_URL='postgres://localhost:5432/flask_todo'

现在在 app.py 中,将数据库 URL 包含在您的应用程序配置中。

app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', '')
db = SQLAlchemy(app)

就像这样,您的应用程序就有了数据库连接!

在 Flask 中定义对象

拥有一个可以对话的数据库是良好的第一步。现在是时候定义一些对象来填充该数据库了。

在应用程序开发中,“模型”指的是某些真实或概念对象的数据表示。例如,如果您正在为汽车经销商构建应用程序,您可以定义一个 Car 模型,该模型封装了汽车的所有属性和行为。

在本例中,您正在构建一个包含任务的待办事项列表,每个任务都属于一个用户。在您深入思考它们彼此之间如何关联之前,首先定义任务和用户的对象。

flask-sqlalchemy 包利用 SQLAlchemy 来设置和告知数据库结构。您将通过继承 db.Model 对象来定义一个将驻留在数据库中的模型,并将这些模型的属性定义为 db.Column 实例。对于每一列,您必须指定数据类型,因此您将数据类型作为第一个参数传递到对 db.Column 的调用中。

由于模型定义占据了与应用程序配置不同的概念空间,因此创建 models.py 以将模型定义与 app.py 分开。Task 模型应构建为具有以下属性

  • id:一个值,它是从数据库中提取的唯一标识符
  • name:任务的名称或标题,用户在列出任务时将看到它
  • note:人们可能想在其任务中留下的任何额外注释
  • creation_date:任务的创建日期和时间
  • due_date:任务到期完成的日期和时间(如果有的话)
  • completed:指示任务是否已完成的方式

给定 Task 对象的此属性列表,应用程序的 Task 对象可以定义如下

from .app import db
from datetime import datetime

class Task(db.Model):
    """Tasks for the To Do list."""
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode, nullable=False)
    note = db.Column(db.Unicode)
    creation_date = db.Column(db.DateTime, nullable=False)
    due_date = db.Column(db.DateTime)
    completed = db.Column(db.Boolean, default=False)

    def __init__(self, *args, **kwargs):
        """On construction, set date of creation."""
        super().__init__(*args, **kwargs)
        self.creation_date = datetime.now()

请注意类构造函数方法的扩展。归根结底,您构建的任何模型仍然是一个 Python 对象,因此必须经过构造才能实例化。重要的是要确保模型实例的创建日期反映其实际创建日期。您可以通过有效地说“当构造此模型的实例时,记录日期和时间并将其设置为创建日期”来显式设置该关系。

模型关系

在给定的 Web 应用程序中,您可能希望能够表达对象之间的关系。在待办事项列表示例中,用户拥有多个任务,并且每个任务仅由一个用户拥有。这是“多对一”关系的示例,也称为外键关系,其中任务是“多”,而拥有这些任务的用户是“一”。

在 Flask 中,可以使用 db.relationship 函数指定多对一关系。首先,构建 User 对象。

class User(db.Model):
    """The User object that owns tasks."""
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.Unicode, nullable=False)
    email = db.Column(db.Unicode, nullable=False)
    password = db.Column(db.Unicode, nullable=False)
    date_joined = db.Column(db.DateTime, nullable=False)
    token = db.Column(db.Unicode, nullable=False)

    def __init__(self, *args, **kwargs):
        """On construction, set date of creation."""
        super().__init__(*args, **kwargs)
        self.date_joined = datetime.now()
        self.token = secrets.token_urlsafe(64)

它看起来与 Task 对象非常相似;您会发现大多数对象都具有相同的基本格式,即类属性作为表列。偶尔,您会遇到一些稍微不同的东西,包括一些多重继承魔法,但这是常态。

现在 User 模型已创建,您可以设置外键关系。对于“多”,设置拥有此任务的 Useruser_id 字段,以及具有该 ID 的 user 对象。还要确保包含一个关键字参数 (back_populates),该参数在任务获得用户作为所有者时更新 User 模型。

对于“一”,为特定用户拥有的 tasks 设置一个字段。与维护 Task 对象上的双向关系类似,在 User 的关系字段上设置一个关键字参数,以便在任务分配给用户时更新任务。

# on the Task object
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
user = db.relationship("user", back_populates="tasks")

# on the User object
tasks = db.relationship("Task", back_populates="user")

初始化数据库

现在模型和模型关系已设置,开始设置您的数据库。Flask 没有自己的数据库管理实用程序,因此您必须编写自己的实用程序(在某种程度上)。您不必对此感到困惑;您只需要一些东西来识别要创建的表以及一些代码来创建它们(或在需要时删除它们)。如果您需要更复杂的东西,例如处理数据库表的更新(即数据库迁移),您将需要研究诸如 Flask-MigrateFlask-Alembic 之类的工具。

setup.py 旁边创建一个名为 initializedb.py 的脚本,用于管理数据库。(当然,它不需要这样称呼,但为什么不给文件名起与其功能相符的名称呢?)在 initializedb.py 中,从 app.py 导入 db 对象,并使用它来创建或删除表。initializedb.py 最终应该看起来像这样

from todo.app import db
import os

if bool(os.environ.get('DEBUG', '')):
    db.drop_all()
db.create_all()

如果设置了 DEBUG 环境变量,则删除表并重建。否则,只需创建一次表即可,您就可以开始了。

视图和 URL 配置

连接整个应用程序所需的最后一部分是视图和路由。在 Web 开发中,“视图”(在概念上)是当应用程序中的特定访问点(“路由”)被命中时运行的功能。这些访问点以 URL 的形式出现:应用程序中功能的路径,这些路径返回一些数据或处理已提供的一些数据。视图将是处理来自给定客户端的特定 HTTP 请求并向该客户端返回一些 HTTP 响应的逻辑结构。

在 Flask 中,视图以函数的形式出现;例如,请参阅上面的 hello_world 视图。为了简单起见,这里再次列出

@app.route('/')
def hello_world():
    """Print 'Hello, world!' as the response body."""
    return 'Hello, world!'

当访问 http://domainname/ 的路由时,客户端会收到响应“Hello, world!”

使用 Flask,当函数由 app.route 装饰时,该函数被标记为视图。反过来,app.route 向应用程序的中心配置添加从指定路由到在访问该路由时运行的函数的映射。您可以使用它来开始构建 API 的其余部分。

首先从一个仅处理 GET 请求的视图开始,并使用 JSON 响应表示所有可访问的路由以及可用于访问它们的方法。

from flask import jsonify

@app.route('/api/v1', methods=["GET"])
def info_view():
    """List of routes for this API."""
    output = {
        '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>'
    }
    return jsonify(output)

由于您希望您的视图处理一种特定类型的 HTTP 请求,因此请使用 app.route 添加该限制。methods 关键字参数将采用字符串列表作为值,每个字符串都是一种可能的 HTTP 方法类型。在实践中,您可以使用 app.route 限制为一种或多种类型的 HTTP 请求,或者通过单独保留 methods 关键字参数来接受任何类型的 HTTP 请求。

您打算从视图函数返回的任何内容必须是字符串或 Flask 在构造格式正确的 HTTP 响应时转换为字符串的对象。此规则的例外情况是当您尝试处理重定向和应用程序抛出的异常时。这对您(开发人员)意味着,您需要能够将您尝试发送回客户端的任何响应封装到可以解释为字符串的内容中。

包含复杂性但仍可以字符串化的良好结构是 Python 字典。因此,我建议,无论何时您想向客户端发送一些数据,您都选择一个 Python dict,其中包含您需要传达信息的任何键值对。要将该字典转换为格式正确的 JSON 响应(包括标头),请将其作为参数传递给 Flask 的 jsonify 函数(from flask import jsonify)。

上面的视图函数获取有效列出此 API 打算处理的每个路由的内容,并在访问 http://domainname/api/v1 路由时将其发送到客户端。请注意,就其本身而言,Flask 支持路由到完全匹配的 URI,因此使用尾部 / 访问同一路由将创建 404 错误。如果您想使用相同的视图函数处理两者,则需要堆叠装饰器,如下所示

@app.route('/api/v1', methods=["GET"])
@app.route('/api/v1/', methods=["GET"])
def info_view():
    # blah blah blah more code

一个有趣的案例是,如果定义的路由带有尾部斜杠,而客户端请求的路由没有斜杠,则您无需重复使用装饰器。Flask 会适当地重定向客户端的请求。奇怪的是它不能双向工作。

Flask 请求和数据库

Web 框架的基本工作是处理传入的 HTTP 请求并返回 HTTP 响应。先前编写的视图实际上与 HTTP 请求没有太多关系,除了访问的 URI。它不处理任何数据。让我们看看当需要处理数据时 Flask 的行为方式。

首先要知道的是,Flask 不会为每个视图函数提供单独的 request 对象。它有一个全局请求对象,每个视图函数都可以使用,并且该对象方便地命名为 request,并且可以从 Flask 包导入。

接下来是 Flask 的路由模式可以有更多细微差别。一种情况是硬编码路由,必须完全匹配才能激活视图函数。另一种情况是路由模式,它可以处理一系列路由,所有路由都映射到一个视图,方法是允许该路由的一部分是可变的。如果所讨论的路由具有变量,则可以从视图的参数列表中同名的变量访问相应的值。

@app.route('/a/sample/<variable>/route)
def some_view(variable):
    # some code blah blah blah

要在视图中与数据库通信,您必须使用在脚本顶部附近填充的 db 对象。当您想进行更改时,其 session 属性是您与数据库的连接。如果您只想查询对象,则从 db.Model 构建的对象通过 query 属性具有自己的数据库交互层。

最后,您希望从视图获得的任何比字符串更复杂的响应都必须经过精心构建。以前,您使用“jsonified”字典构建响应,但做出了一些假设(例如,200 状态代码、状态消息“OK”、Content-Type 为“text/plain”)。您想要在 HTTP 响应中添加的任何特殊内容都必须经过精心添加。

了解有关使用 Flask 视图的这些事实,您可以构建一个视图,其工作是创建新的 Task 对象。让我们看一下代码(如下),并逐个分析。

from datetime import datetime
from flask import request, Response
from flask_sqlalchemy import SQLAlchemy
import json

from .models import Task, User

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', '')
db = SQLAlchemy(app)

INCOMING_DATE_FMT = '%d/%m/%Y %H:%M:%S'

@app.route('/api/v1/accounts/<username>/tasks', methods=['POST'])
def create_task(username):
    """Create a task for one user."""
    user = User.query.filter_by(username=username).first()
    if user:
        task = Task(
            name=request.form['name'],
            note=request.form['note'],
            creation_date=datetime.now(),
            due_date=datetime.strptime(due_date, INCOMING_DATE_FMT) if due_date else None,
            completed=bool(request.form['completed']),
            user_id=user.id,
        )
        db.session.add(task)
        db.session.commit()
        output = {'msg': 'posted'}
        response = Response(
            mimetype="application/json",
            response=json.dumps(output),
            status=201
        )
        return response

让我们从 @app.route 装饰器开始。路由是 '/api/v1/accounts/<username>/tasks',其中 <username> 是路由变量。将尖括号放在您想要可变的路由的任何部分周围,然后在下一行在参数列表中包含路由的该部分,名称相同。参数列表中应包含的唯一参数应该是您路由中的变量。

接下来是查询

user = User.query.filter_by(username=username).first()

要按用户名查找一个用户,从概念上讲,您需要查看数据库中存储的所有 User 对象,并查找用户名与请求的用户名匹配的用户。使用 Flask,您可以通过 query 属性直接向 User 对象询问与您的条件匹配的实例。这种类型的查询将提供对象列表(即使它只有一个对象或根本没有对象),因此要获取您想要的对象,只需调用 first() 即可。

task = Task(
    name=request.form['name'],
    note=request.form['note'],
    creation_date=datetime.now(),
    due_date=datetime.strptime(due_date, INCOMING_DATE_FMT) if due_date else None,
    completed=bool(request.form['completed']),
    user_id=user.id,
)

无论使用哪种 HTTP 方法,当数据发送到应用程序时,该数据都存储在 request 对象的 form 属性中。前端字段的名称将是映射到 form 字典中该数据的键的名称。它始终以字符串的形式出现,因此如果您希望您的数据是特定的数据类型,则必须通过将其强制转换为适当的类型来显式说明。

另一件需要注意的事情是将当前用户的用户 ID 分配给新实例化的 Task。这就是维护外键关系的方式。

db.session.add(task)
db.session.commit()

创建新的 Task 实例很棒,但是它的构造与数据库中的表没有内在联系。为了在相应的 SQL 表中插入新行,您必须使用附加到 db 对象的 sessiondb.session.add(task) 将新的 Task 实例暂存为添加到表中,但尚未添加。虽然这里只完成一次,但您可以在提交之前添加任意数量的内容。db.session.commit() 获取所有暂存的更改或“提交”,并将它们应用于数据库中的相应表。

output = {'msg': 'posted'}
response = Response(
    mimetype="application/json",
    response=json.dumps(output),
    status=201
)

响应是 Response 对象的实际实例,其 mimetype、正文和 status 是有意设置的。此视图的目标是提醒用户他们创建了新内容。看到此视图应该是一个发送和接收 JSON 的后端 API 的一部分,响应正文必须是 JSON 可序列化的。带有简单字符串消息的字典应该足够了。通过对您的字典调用 json.dumps 来确保它已准备好传输,这将把您的 Python 对象转换为有效的 JSON。这代替了 jsonify 使用,因为 jsonify 使用其输入作为响应正文来构造实际的响应对象。相比之下,json.dumps 只是获取给定的 Python 对象,并在可能的情况下将其转换为有效的 JSON 字符串。

默认情况下,使用 Flask 发送的任何响应的状态代码都将为 200。这适用于大多数情况,在这些情况下,您不会尝试发送回特定的重定向级别或错误级别消息。由于这种情况明确地让前端知道何时创建了新项目,因此将状态代码设置为 201,这对应于创建新事物。

就是这样!这是一个基本的视图,用于在 Flask 中创建新的 Task 对象,给定您的待办事项列表应用程序的当前设置。可以构建类似的视图来列出、编辑和删除任务,但此示例提供了如何完成它的想法。

更大的图景

应用程序中包含的内容远不止一个用于创建新事物的视图。虽然我没有讨论任何关于授权/身份验证系统、测试、数据库迁移管理、跨域资源共享等的内容,但上面的详细信息应该为您提供足够的知识来开始深入研究构建您自己的 Flask 应用程序。

PyCon Cleveland 2018 上了解更多 Python 信息。

3 条评论

有趣,但是太长了,如果代码可以在 Github 或其他地方提供,以便我们可以跟进,那就太好了

非常感谢本教程。
期待完整版本。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.