如果您正在使用 Python 开发 Web 应用程序,那么您很可能正在利用框架。框架“是一个代码库,通过为常见操作提供可重用的代码或扩展,使开发人员在构建可靠、可扩展和可维护的 Web 应用程序时的工作更轻松”。Python 有许多框架,包括 Flask、Tornado、Pyramid 和 Django。新的 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 应用程序。app
是 Flask
的一个实例,它接收脚本文件的 __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://127.0.0.1:5432/flask_todo'
db = SQLAlchemy(app)
但是,这不是一个可持续的解决方案。如果您更改数据库或不希望您的数据库 URL 在源代码控制中可见,您将必须采取额外的步骤来确保您的信息适合环境。
您可以使用环境变量使事情更简单。它们将确保,无论代码在什么机器上运行,如果该内容在运行环境中配置,它始终指向正确的内容。它还确保,即使您需要该信息才能运行应用程序,它也永远不会在源代码控制中显示为硬编码值。
在您声明 FLASK_APP
的同一位置,声明一个 DATABASE_URL
指向您的 Postgres 数据库的位置。开发通常在本地进行,因此只需指向您的本地数据库即可。
# also in your activate script
export DATABASE_URL='postgres://127.0.0.1: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 模型已创建,您可以设置外键关系。对于“多”,设置拥有此任务的 User
的 user_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-Migrate 或 Flask-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
关键字参数来接受任何类型。
您打算从视图函数返回的任何内容必须是一个字符串或一个对象,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
对象的 session
。db.session.add(task)
暂存要添加到表中的新 Task
实例,但尚未添加。虽然这里只完成一次,但您可以在提交之前添加任意数量的内容。db.session.commit()
获取所有暂存的更改,或“提交”,并将它们应用于数据库中的相应表。
output = {'msg': 'posted'}
response = Response(
mimetype="application/json",
response=json.dumps(output),
status=201
)
响应是 Response
对象的实际实例,其 mimetype
、body 和 status
都是经过深思熟虑设置的。此视图的目标是提醒用户他们创建了新内容。鉴于此视图应该是一个后端 API 的一部分,该 API 发送和接收 JSON,响应正文必须是 JSON 可序列化的。带有简单字符串消息的字典应该足够了。通过在字典上调用 json.dumps
来确保它已准备好传输,这将把您的 Python 对象转换为有效的 JSON。这代替了 jsonify
使用,因为 jsonify
使用其输入作为响应正文来构造实际的响应对象。相比之下,json.dumps
只是接受给定的 Python 对象,并在可能的情况下将其转换为有效的 JSON 字符串。
默认情况下,使用 Flask 发送的任何响应的状态代码都将为 200
。这适用于大多数情况,在大多数情况下,您不会尝试发回特定的重定向级别或错误级别消息。由于这种情况显式地让前端知道何时创建了新项目,因此将状态代码设置为 201
,这对应于创建新事物。
就这样!这是一个基本的视图,用于在 Flask 中创建新的 Task
对象,前提是您的待办事项列表应用程序的当前设置。可以构建类似的视图来列出、编辑和删除任务,但此示例提供了一个关于如何完成它的想法。
更大的图景
一个应用程序包含的内容远不止一个用于创建新事物的视图。虽然我没有讨论关于授权/身份验证系统、测试、数据库迁移管理、跨域资源共享等任何内容,但上面的详细信息应该为您提供足够的开始深入研究构建您自己的 Flask 应用程序的知识。
在 PyCon Cleveland 2018 了解更多 Python 信息。
3 条评论