avatar


8.Flask [1/2]

什么是Flask

Flask,Python开发中的后端框架。

在Python开发中,另一个常见的后端框架是Django。

  • Django是个大而全的框架。
    内部有非常多的组件:orm、session、cookie、admin、form、modelform、路由、视图、模板,中间件、分页、auth、contenttvpe、缓存、信号、多数据库连接等。
  • flask是一个轻量级的框架。
    框架本身没有太多的功能,只有:路由、视图、模板(jinja2)、session、中间件。但是,第三方组件非常齐全。

在请求处理方面,Django的请求处理是逐一封装和传递,Flask的请求处理是利用上下文管理来实现的。在下文我们会看到具体的体现。

安装

安装命令:pip install flask

示例代码:

1
pip install flask

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Looking in indexes: https://mirrors.aliyun.com/pypi/simple/
Collecting flask
Downloading https://mirrors.aliyun.com/pypi/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl (99 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 99.7/99.7 kB 696.5 kB/s eta 0:00:00
Collecting click>=8.1.3
Downloading https://mirrors.aliyun.com/pypi/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl (97 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 97.9/97.9 kB 702.7 kB/s eta 0:00:00
Collecting Jinja2>=3.1.2
Downloading https://mirrors.aliyun.com/pypi/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl (133 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.1/133.1 kB 701.8 kB/s eta 0:00:00
Collecting itsdangerous>=2.1.2
Downloading https://mirrors.aliyun.com/pypi/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting Werkzeug>=3.0.0
Downloading https://mirrors.aliyun.com/pypi/packages/c3/fc/254c3e9b5feb89ff5b9076a23218dafbc99c96ac5941e900b71206e6313b/werkzeug-3.0.1-py3-none-any.whl (226 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 226.7/226.7 kB 696.2 kB/s eta 0:00:00
Collecting blinker>=1.6.2
Downloading https://mirrors.aliyun.decom/pypi/packages/bf/2b/11bcedb7dee4923253a4a21bae3be854bcc4f06295bd827756352016d97c/blinker-1.6.3-py3-none-any.whl (13 kB)
Collecting MarkupSafe>=2.0
Downloading https://mirrors.aliyun.com/pypi/packages/12/b3/d9ed2c0971e1435b8a62354b18d3060b66c8cb1d368399ec0b9baa7c0ee5/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)
Installing collected packages: MarkupSafe, itsdangerous, click, blinker, Werkzeug, Jinja2, flask
Successfully installed Jinja2-3.1.2 MarkupSafe-2.1.3 Werkzeug-3.0.1 blinker-1.6.3 click-8.1.7 flask-3.0.0 itsdangerous-2.1.2

注意安装过程,有两个依赖:

  • Jinja2
    提供模板功能
  • Werkzeug
    提供wsgi的功能,Web Server Gateway Interface,Web服务网关接口。

入门案例

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask

app = Flask(__name__)


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


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

运行结果:

1
2
3
4
5
 * Serving Flask app 'ft1'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit

然后,我们访问http://127.0.0.1:5000/index

index

Werkzeug

如果我们点进app.run()方法,会在run方法中,找到这么一段代码:

1
2
3
4
5
6
7
8
9
from werkzeug.serving import run_simple

try:
run_simple(t.cast(str, host), port, self, **options)
finally:
# reset the first request information if the development server
# reset normally. This makes it possible to restart the server
# without reloader and that stuff from an interactive shell.
self._got_first_request = False

这就是Werkzeug在Flask中发挥的作用。

我们可以直接基于Werkzeug实现一个Web服务。示例代码:

1
2
3
4
5
6
7
8
9
10
11
from werkzeug.serving import run_simple
from werkzeug.wrappers import Response


def func(environ, start_response):
response = Response('Hello World!')
return response(environ, start_response)


if __name__ == '__main__':
run_simple(hostname='127.0.0.1', port=9000, application=func)

运行结果:

1
2
3
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:9000
Press CTRL+C to quit

然后,我们访问http://127.0.0.1:9000,能看到Hello World!

如果我们把上述代码改一下,自定义一个Flask类,run_simpleapplication填Flask类的对象,那么最终会调用__call__方法。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from werkzeug.serving import run_simple
from werkzeug.wrappers import Response


class Flask(object):

def __call__(self, environ, start_response):
response = Response('Hello World!')
return response(environ, start_response)


app = Flask()

if __name__ == '__main__':
run_simple(hostname='127.0.0.1', port=9000, application=app)

运行结果:

1
2
3
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:9000
Press CTRL+C to quit

如果我们再改一下,讲run_sample放在Flask类中,通过成员方法run调用run_sample。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from werkzeug.serving import run_simple
from werkzeug.wrappers import Response


class Flask(object):

def __call__(self, environ, start_response):
response = Response('Hello World!')
return response(environ, start_response)

def run(self):
run_simple(hostname='127.0.0.1', port=9000, application=self)


app = Flask()

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

运行结果:

1
2
3
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:9000
Press CTRL+C to quit

到这一步,就非常像Flask了。

特别的,我们能在flask的app.py中,找到__call__,示例代码:

1
2
3
4
5
6
def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app`, which can be
wrapped to apply middleware.
"""
return self.wsgi_app(environ, start_response)

self.wsgi_app(environ, start_response),再往内部,就是Flask的内部源码了,我们会在后续的文章中进行更详细的讨论。

响应

返回文本

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask

app = Flask(__name__)


@app.route('/res')
def res():
return 'res'


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

返回JSON

返回JSON,需要依赖jsonify,在flask包中。

1
from flask import jsonify

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, jsonify

app = Flask(__name__)


@app.route('/res')
def res():
res_l = [{'a': 1}, {'b': 2}]
return jsonify(res_l)


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

template

返回模板页面,需要依赖render_template,在flask包中。

1
from flask import render_template

模版默认位于templates目录下,可以在app = Flask()中根据template_folder参数进行配置。

template

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, render_template

app = Flask(__name__)


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


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

关于模板,内容很多,包括如何给模版传参等。但是鉴于在实际开发中,几乎不用,我们不讨论。

接收请求的方式

现象

假设存在一个模板如下,其中表单的方法是post

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<h1>用户登录</h1>
<form method="post">
<input type="text" name="uid"/>
<input type="text" name="pwd"/>
<input type="submit" name="提交">
</form>
</body>
</html>

点击登录后,会返回如下,Method Not Allowed

用户登录

解决

这是因为@app.route()默认支持的方式是GET,如果需要支持其他方式,需要我们指定methods

1
@app.route('/login', methods=['GET', 'POST'])

获取参数

request对象

在本文开头,我们说"在请求处理方面,Django的请求处理是逐一封装和传递,Flask的请求处理是利用上下文管理来实现的"。

在这里,就会看到具体的体现。

获取参数不依赖login方法的形参(传递给login方法的参数),而是依赖request对象,直接从request对象中获取。

1
from flask import request

获取GET请求参数

在采用GET请求方式传递参数时,参数直接拼接在URL中,可以如下两种方式获取:

  • request.args.get('key')
  • request.values.get('key')

假设存在一个请求如下:

1
curl --location 'http://127.0.0.1:5000/req?key=qwert'

获取参数,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, render_template, request

app = Flask(__name__)


@app.route('/req', methods=['GET', 'POST'])
def req():
print(request.args.get('key'))
print(request.values.get('key'))


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

运行结果:

1
2
qwert
qwert

获取JSON参数

获取JSON参数有两种方法,两种方法获取到的都是字典格式,不需要再进行json.loads()

  • request.get_json()
  • request.json

假设存在一个请求如下:

1
2
3
4
5
6
curl --location 'http://127.0.0.1:5000/login' \
--header 'Content-Type: application/json' \
--data '{
"k1":"v1",
"k2":"v2"
}'

获取参数,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, request

app = Flask(__name__)


@app.route('/req', methods=['GET', 'POST'])
def req():

print(request.get_json())
print(type(request.get_json()))
print(request.json)
print(type(request.json))


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

运行结果:

1
2
{'k1': 'v1', 'k2': 'v2'}
{'k1': 'v1', 'k2': 'v2'}

获取表单数据

获取表单数据,基于request.form

获取某个具体值的方法为:

  • request.form.get('k1')
  • request.form['k1']
1
2
3
4
curl --location 'http://127.0.0.1:5000/req' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'k1=v1' \
--data-urlencode 'k2=v2'

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, render_template, request

app = Flask(__name__)


@app.route('/req', methods=['GET', 'POST'])
def req():
print(request.form)
print(request.form.get('k1'))
print(request.form['k1'])


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

运行结果:

1
2
3
ImmutableMultiDict([('k1', 'v1'), ('k2', 'v2')])
v1
v1

解决问题

回到上文的现象,我们接受表单的POST请求。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, render_template, request

app = Flask(__name__)


@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')

uid = request.form.get('uid')
pwd = request.form.get('pwd')

if uid == 'kaka' and pwd == 'pwd':
return '登录成功'
else:
return '登录失败'


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

解释说明:request.method,获取调用方法

RESTful

关于什么是RESTful,可以参考《基于Java的后端开发入门:17.SpringMVC》
这里主要讨论在Flask中,如何实现RESTful

假设存在一个请求,如下:

1
curl --location 'http://127.0.0.1:5000/delete/123'

获取参数,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask

app = Flask(__name__)


@app.route('/delete/<idv>', methods=['GET', 'POST'])
def delete(idv):
print(idv)
print(type(idv))


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

运行结果:

1
2
123
<class 'str'>

默认数据类型是str。如果我们想指定参数的数据类型,例如指定为int,可以在前面填上int:。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask

app = Flask(__name__)


@app.route('/delete/<int:idv>', methods=['GET', 'POST'])
def delete(idv):
print(idv)
print(type(idv))


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

运行结果:

1
2
123
<class 'int'>

redirect、endpoint和url_for

现在,我们想登录成功后,跳转到新的页面。

基于redirect

1
from flask import redirect

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask import Flask, render_template, request, redirect

app = Flask(__name__)


@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')

uid = request.form.get('uid')
pwd = request.form.get('pwd')

if uid == 'kaka' and pwd == 'pwd':
return redirect('/index')
else:
return '登录失败'


@app.route('/index', endpoint='idx')
def index():
return 'ok'


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

此外,我们还可以在@app.route()中,指定endpoint参数的值,并用url_for方法解析。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)


@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')

uid = request.form.get('uid')
pwd = request.form.get('pwd')

if uid == 'kaka' and pwd == 'pwd':
return redirect(url_for('idx'))
else:
return '登录失败'


@app.route('/index', endpoint='idx')
def index():
return 'ok'


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

注意,endpoint有两个特点:

  1. 不可重复
  2. 默认为函数名

(所以,有时候我们发现报错,endpoint重复,可能是我们指定的名称和另一个函数的名称重复了。)

Session

如何使用Session

关于什么是Session,可以参考《基于Java的后端开发入门:13.Servlet、Filter和Listener》的"Session"部分。
这里主要讨论在Flask中,如何实现会话(Session)。

一共三步:

  1. from flask import session
  2. app.secret_key = '123456',指定Session的key。
  3. 在需要设置Session的地方,进行设置。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from flask import Flask, render_template, request, redirect, url_for, session

app = Flask(__name__)

app.secret_key = '123456'

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')

uid = request.form.get('uid')
pwd = request.form.get('pwd')

if uid == 'kaka' and pwd == 'pwd':
session['uid'] = uid
return redirect(url_for('idx'))
else:
return '登录失败'


@app.route('/index', endpoint='idx')
def index():
return 'ok'


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

如果我们没有指定app.secret_key,会有如下的报错:

1
2
3

RuntimeError: The session is unavailable because no secret key was set. Set the secret_key on the application to something unique and secret.
127.0.0.1 - - [25/Oct/2023 20:54:26] "POST /login HTTP/1.1" 500 -

装饰器

上述存在一个问题,我们需要在每一个需要校验session的方法前,都加上

1
2
if not session.get('uid'):
return 'error'

《基于Python的后端开发入门:3.拷贝、类型注解、闭包和一些常用的包》,我们讨论过装饰器,可以自定义一个装饰器。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from flask import Flask, render_template, request, redirect, url_for, session
import functools

app = Flask(__name__)

app.secret_key = 'ajldjajdajdjasldjl'


def auth(func):
@functools.wraps(func)
def inner():
if not session.get('uid'):
return 'error'
return func()
return inner


@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')

uid = request.form.get('uid')
pwd = request.form.get('pwd')

if uid == 'kaka' and pwd == 'pwd':
session['uid'] = uid
return redirect(url_for('idx'))
else:
return '登录失败'


@app.route('/index', endpoint='idx')
@auth
def index():
return 'ok'


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

解释说明:

  • @app.route('/index'),会读取函数,并将函数名作为endpoint的名字。
    如果装饰器没加functools,就可能会导致endpoint重复。
  • 如果加了我们自定义的装饰器在@app.route的前面,同样可能会导致endpoint重复。

蓝图

蓝图,构建业务功能可拆分的目录结构。

如下,是一个蓝图目录结构:
蓝图目录结构

v1.py,第一个蓝图,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Blueprint

v1_app = Blueprint('v1', __name__)


@v1_app.route('/f1')
def f1():
return 'f1'


@v1_app.route('/f2')
def f2():
return 'f2'

v2.py,第二个蓝图,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Blueprint

v2_app = Blueprint('v2', __name__)


@v2_app.route('/f1')
def f1():
return 'f1'


@v2_app.route('/f2')
def f2():
return 'f2'

__init__.py,构造一个Flask的对象,并把上文的两个蓝图添加进去。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask


def create_app():
app = Flask(__name__)
app.secret_key = '123456'

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

from .views import v1
app.register_blueprint(v1.v1_app, url_prefix='/admin')

from .views import v2
app.register_blueprint(v2.v2_app, url_prefix='/web')

return app

manage.py,程序的主入口,示例代码:

1
2
3
4
5
6
from flask_demo import create_app

app = create_app()

if __name__ == '__main__':
app.run()
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10908
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区