Flask 入门

安装 Flask

创建虚拟环境

创建好你的应用目录, 然后我们就可以安装 Flask 了.

安装 Flask 的最简便的方法是使用虚拟环境, 虚拟环境可以理解为 Python 解释器的一个 copy, 这个环境里安装其他包不会对全局 Python 解释器产生影响.

你可能会好奇我们为什么要大费周章创建一个虚拟环境. 主要原因是: 环境的兼容问题.

在 Unix 系统下, 我们需要先装下 virtualenv 或者 python3-venv 包:

$ sudo apt-get install python3-venv

创建虚拟环境:

$ python3 -m venv <name>

通常 <name> 我们取 .venv 或者 venv. 当这个命令执行完毕后, 我们就会有一个 .venv 的子目录, 里面包含一个全新的 Python 解释器.

然后激活这个虚拟环境

$ source venv/bin/activate

然后你应该能在命令提示符前看到多出来的环境名了.

一键安装

安装 Flask 非常简单, 我们利用 pip 即可:

(.venv) $ pip install flask

检验一下:

(.venv) $ python3
>>> import flask
>>>

Flask Quick Start

在根目录下新建一个 app.py, 然后就可以开始我们的编码之旅了!

应用实例

任何 Flask 应用都需要创建一个应用实例.

Web 服务器使用一种名为 Web 服务器网关接口(WSGI, Web server gateway interface, 读作 wiz-ghee) 的协议, 把接受自客户端的所有请求都转交给这个对象处理. 应用实例是 Flask 类的对象, 一般我们用下面的方式构建

from flask import Flask
app = Flask(__name__)

这个类的构造函数需要传入一个应用主模块的名称.

你可能会好奇创建应用实例是干什么的. 你可以做下面的一个小实验:

>>> from flask import Flask, current_app, request
>>> app = Flask('hello')
>>> app
>>> current_app
>>> with app.app_context():
    print(repr(current_app))
>>> with app.test_request_context():
    print(repr(request))

看看 Flask 的核心代码:

def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any:
        ctx = self.request_context(environ)
        error: t.Optional[BaseException] = None
        try:
            try:
                ctx.push()
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
...

我们之后会讲到通过蓝图构建应用实例. 不过对于小型项目来说, 这样做就够了.

路由和视图函数

  • 路由(route): 处理 URL 和函数之间关系的程序

    • 简单来说, 就是让应用实例知道访问某个 URL 要运行哪些代码

  • 我们可以用类装饰器 app.route() 来声明路由.

    • 下面的例子: 访问根目录执行 index 函数.

    • 当然也可以用 app.add_url_rule(), 但是麻烦很多.

@app.route('/')
def index():
    return '<h1>Hello World!</h1>'

index 这个名字你应该不陌生. 除了下标的含义, 它还代表类似于主页的含义. 它处理入站请求(request), 然后执行 index 函数. 它的返回值称为响应(response), 可以是包含 HTML 元素的简单字符串.

URL是可变的, app.route 它可以 parse 一些特定的句法(也叫做 converter), 例如

@app.route('/user/<name>')
def user(name):
    return f'my name is {name}'

Flask 会把这个动态部分作为函数参数传入函数. 现在, 访问 /user/besthope, 就会输出我们的预期结果.

这个动态部分支持强调类型(默认是 string). 例如, <int:id> 就声明了它只匹配动态片段 id 为整数的情况(注意这和 type hint 的语法不太一样).

我们还可以获取视图函数对应的 URL, 用 url_for 即可:

>>> from flask import url_for
>>> url_for('index')

命令行选项

运行我们的 Flask 程序, 你会发现我们用的是一个命令行指令 flask run. 这个其实就是 Flask 做的一个命令行指令, 其实等价于你在脚本里写一个

if __name__ == '__main__':
    app.run()

我们不需要像其它语言一样写一个 main 函数! 用命令行去 run 一下.

这个命令会在 FLASK_APP 环境变量指定的 Python 文件里寻找应用实例. 默认这个文件是 app.py. 如果你没有按照我们上面的指示, 就会遇到相关的错误. 如果你想修改这个默认值, 可以使用下面的语句

(.venv) $ export FLASK_APP=hello.py

其它的一些命令行选项, 例如调试模式, 你可以

(.venv) $ export FLASK_DEBUG=1

千万不要在生产服务器中启用调试模式!

客户端通过调试器能请求执行远程代码,因此可能导致生产服务器遭到攻击。作为一种简单的保护措施,启动调试模式时可以要求输入PIN 码,执行 flask run 命令时会打印在控制台中。

我们推荐你使用 config 文件或者利用 dotenv 去配置这些选项. 不过到目前, 你只需要了解这些部分即可.

你可以试试 flask help, 里面有更多关于 flask 命令的相关选项.

例如 --host 选项, 它可以让 Web 服务器监听公共网络接口上的连接, 同一网络中的其它计算机发送的连接也可以接受到:

(venv) $ flask run --host 0.0.0.0
* Serving Flask app "hello"
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

模板

视图函数的作用很明确, 即生成请求的响应, 但很多情况下, 请求会改变应用的状态, 而这种变化仍然发生在视图函数中.

举个例子, 你要实现一个注册账户的功能, 那么首先需要让用户填写用户名和密码, 然后点击提交按钮. 服务器接受到包含用户输入数据的请求, 然后 Flask 把请求分派给处理注册请求的视图函数. 这个视图函数需要访问数据库, 添加新用户, 然后生成响应回送浏览器, 指明操作成功还是失败. 这两个过程分别称为业务逻辑表现逻辑.

把业务逻辑和表现逻辑混在一起会导致代码难以理解和维护. 你的 Python 代码可能要充斥大量的 HTML 代码. 这并不是我们希望的事情. 于是就有了模板:

  • 它用来存放这些表现逻辑.

但实际上, 这些表现逻辑并不需要我们后端实现. 在前后端分离的概念里, 前端就负责这些用户的输入逻辑.

学会用 Jinja2 去渲染模板, 我们就可以去编写一些前后端不分离的 Web 应用. 简单介绍这部分:

  • 新建一个 templates 文件夹. 在模板文件夹内, 新建一个 index.html.

  • 内容: <h1>hello world</h1>

然后在视图函数里, 我们返回一个 render_template().

@app.route('/')
def index():
    return render_template('index.html')

示例: Glimpse to RESTful APIs

REST 架构

在本次的示例中, 我们会演示怎么去编写符合 REST(REpresentational State Transfer) 原则的接口.

REST 对信息的核心抽象就是资源(Resources), 通常以 URI(统一资源定位符) 的形式表现. 这里的资源可以是文本, 图片, 或者任何能被命名的信息. 换句话说, 如果某一信息能被超链接导航到, 那它就符合资源的定义. 要访问某一"资源", 访问它的 URI 即可. URI 就是这个"资源"独一无二的标识符!

"资源"是对信息的抽象: 它代表的是一组相关的实体, 而非一个单独的实体. 具体的实体会发生变化, 但"资源"表示实体的结构和行为是不变的.

例如 Github 对用户信息封装了一个接口

https://api.github.com/users/besthope-official

"资源"具体表现的形式, 就称作它的"表现层"(Representation).

例如, 文本可以用 txt 格式表现, 也可以用 JSON, xml 格式, 甚至二进制格式表现; 图片可以用 jpg 格式表现, 也可以用 png 格式表现.

上面的 URI 访问的结果就是一个 JSON 表示:

{
  "login": "Besthope-Official",
  "id": 29171517,
  "node_id": "MDQ6VXNlcjI5MTcxNTE3",
  ...
}

想要获取同一资源另外的表现层, 例如上面的XML表示, 你需要为这个请求附带额外信息, 这个额外信息就叫做标头 (header). 在这个例子中, 你需要设置下 Content-Type 这个header.

访问一个网站, 就代表了客户端和服务器的一个互动过程. 在这个过程中, 必然涉及数据和状态的改变.

互联网通信协议HTTP 协议, 是一个无状态协议. 所有的状态都保存在服务器端, 如果客户端想要操作服务器, 必须通过某种途径, 使得服务器端发生"状态转移"(State Transfer). 而这种状态转移是建立在表现层之上的. 所以, 你现在就能明白 REST 到底指的是个啥.

至于客户端用到的手段, 只能是HTTP 的请求方法(request methods). 下面这五种方法对应的 CRUD(增删改查) 操作:

根据 HTTP 协议, 请求方法由客户端提出, 由服务器决定是否执行或拒绝. 例如, 你发起了一个 GET 请求, 想获取 /clients 下的资源. 但这个文件获取需要有权限, 服务器就可以有如下的执行方式:

  • 404 Not found: 这个状态码表示文件不存在. 当然是服务器装的.

  • 403 Forbidden: 这个文件拒绝访问.

  • 405 Method Not Allowed: 然后用 Allow 头告诉你可以用 HEAD 方法获取文件的元信息.

这样, 你就不难理解: 资源都是名词. 例如, 下面的这个 URI 就是错误的

http://api.example.com/posts/delete/233/

要实现删除操作, 你需要用到上面介绍的请求方法, 用动词+名词的方式去实现这个操作, 例如

DELETE https://api.github.com/gists/gists/1

返回的结果也不再是资源, 而是我们上面提到的返回码和原因短语. 通常来说, 我们会在后端文档中指明各个返回码的含义, 例如上面这个例子 204 代表 No Content:

Status: 204

讲了这么多有的没的, 那采用 RESTful 架构到底有什么用. 为什么, 那就是大家都用这一套. 正所谓某个商品销量高, 评价又好, 必然有它的原因. 它好理解, 跨平台, 支持缓存, 可拓展......

不过说到底, 它也只是一种架构. 一种准则. 你完全可以不按照这套方式去设计接口(例如去学习RPC等其它架构方式), 完全的放飞自我, 只不过这样, 你的 URL 设计会变得一团糟:

  • URI 又臭又长: /authors/12/categories/2

    • 用字符串查询去优化: /authors/12?categories=2

  • URI 的命名: order/create-order

    • 不要用单数名词去命名资源集合, 也不要出现动宾结构的出现.

  • .....

Coding Time

Flask 是编写 RESTful APIs 的一个很好的选择, 不仅因为是它好写(Python 的优势), 同时, Flask 本身对 REST 的请求和调度支持很好. 从我个人的角度来说, Flask 小而微, 由社区 extensions 驱动, 和 vscode 类似, 非常符合我的审美需求:>

我们用一个简单的增删改查的例子来体现这一点: 下载我们的示例代码, 它包含了一个简单的框架和存储的数据.

在后期的教学以及作业布置, 我们会经常采用类似的方式.

如果你是 WSL, 你可以通过 Windows Explorer 直接将文件复制到你想要的目录下(试试 explorer.exe .)

不过, 为了熟悉 Unix 工作流, 我们推荐你使用 wget 将文件下载到当前目录下

wget https://github.com/Besthope-Official/Singularity-backend/raw/main/demo/demo_crud.zip

然后利用 unzip 指令进行解压:

unzip demo_curd.zip

我们的数据保存在 db.py, 它是一个由字典构成的列表, 每一个字典有 4 个字段, id, name, group, category, 作为键存在.

我们用 get_author() 作为示例:

  • 它返回"数据库"里指定 group 和指定 category 的作者信息.

  • 请求 URL 是: http://127.0.0.1:5000/authors/<group_id>, 请求方式为 GET.

  • 它需要一个请求 Query 参数 categories, 例如 /authors/1?categories=1

获取这个请求参数, 我们可以利用 request, 它是一个全局的 Request object, 你只用导入它就可以获取请求(根据上下文). 同时, 它还是线程安全的!

剩下一个问题是, 返回的请求是一段 JSON, 你可能会想, 直接返回一个 json.dumps 不就可以了(就和我们上面返回HTML标签一样). 其实你这是隐式创建了一个 Response.

但假如我们想额外返回例如错误码之类的信息, 你可能还要在 JSON 里添加一个错误码的字段.

我们不想让 JSON 的格式变得太复杂, 如果它能作为一个类似 header 的返回, 那会更方便. 很显然我们是没有 flask.response 的, 但显式地创建一个 response 也不是什么难事:

resp = Response(js, status=200, mimetype='application/json')
return resp

其实我们还有更方便的 jsonify 多做了一层封装:

@app.route('/authors/<int:group_id>', methods=['GET'])
def get_author(group_id):
    '''
    Return the authors that are in the same group and category.
    '''
    category = request.args.get('categories', None)
    
    result_authors = [author for author in authors if author['group']
                      == group_id and author['category'] == int(category)]
    
    if not result_authors:
        js = jsonify({'message' : 'No authors to be found'})
        js.status_code = 404
    else:
        js = jsonify(result_authors)
        js.status_code = 200
        
    return js

用 curl 去获取请求(-i表示显示headers):

curl -i http://127.0.0.1:5000/authors/1?categories=1

学习资料

最后更新于