Flask框架基础
一、框架介绍
Flask诞生于2010年,是Armin ronacher(人名)用 Python 语言编写的 轻量级Web开发框架。
Flask 本身相当于一个内核, 主要实现了路由分发和模板渲染功能, 分别集成自 Werkzeug 和 Jinja2模块包, 这两个也是Flask框架的核心。
虽然核心精简, 但flask提供了非常好的扩展机制, 开发中的各类需求基本都有对应的官方/第三方扩展可以实现, 甚至连自己动手实现也很简单。
1、Flask常用扩展包
- Flask-SQLalchemy:ORM操作数据库;
- Flask-RESTful:开发REST API的工具;
- Flask-Session:Session存储;
- Flask-Migrate:管理迁移数据库;
- Flask-Caching:缓存;
- Flask-WTF:表单;
- Flask-Mail:邮件;
- Flask-Login:认证用户状态;
- Flask-OpenID:认证;
- Flask-Admin:简单而可扩展的管理接口的框架
- Flask-Bable:提供国际化和本地化支持,翻译;
- Flask-Bootstrap:集成前端Twitter Bootstrap框架;
Flask-Moment:本地化日期和时间;
2、web框架对比
框架轻重
- 重量级的框架:包含全家桶式丰富的工具,方便业务程序的快速开发,如Django
- 轻量级的框架:只提供Web框架的核心功能,自由灵活、方便高度定制,如Flask、Tornado
3、环境安装
目前官方最新版1.1.1, 课程中学习 稳定版1.0.2, 以便更好的兼容旧项目
# 创建虚拟环境 mkvirtualenv flask_env -p python3 # 安装flask包 pip install flask==1.0.2
4、文档
二、基本使用
1、最小应用
- 导入Flask类
from flask import Flask- 创建Flask对象,接受一个参数__name__,它会指向程序所在的包
# 1.创建Flask应用
app = Flask(__name__)- 装饰器的作用是将路由映射到视图函数index
# 3.定义路由 @app.route('/') def index(): return 'hello flask'
- Flask应用程序实例的run方法启动web服务器
- 可以指定运行的主机ip地址,端口,是否开启调试模式
if __name__ == '__main__': # 运行应用 (启动一个测试服务器, 接收请求并调用对应的视图函数) app.run(host='0.0.0.0', port=8000, debug=True)
app.run的参数:
host: 绑定的ip(域名) 0.0.0.0port: 监听的端口号debug: 是否开启调试模式1> 可以在网页上显示python错误2> 更新代码后测试服务器自动重启
1.x版本的启动方式
- flask1.0开始, 类似Django, 通过终端执行封装的脚本命令
flask run来运行应用, 不再需要调用app.run() - 新的形式更加有利于 灵活修改环境配置
export FLASK_APP=xx.py # 指定flask应用所在的文件路径 export FLASK_ENV=development # 设置项目的环境, 默认是生产环境 flask run -h 0.0.0.0 -p 8000 # 启动测试服务器并接受请求
- 环境变量相当于 系统的全局变量, 所有程序可用
# 获取环境变量 export # 查看所有环境变量 echo $环境变量 # 查看指定的环境变量 # 设置环境变量 export 环境变量名=值 # 给本次会话设置环境变量, 一旦终端关闭, 环境变量会清空 # 将环境变量写入配置文件(.bashrc/.bash_profile), 重新打开终端时再次加载环境变量
2、路由
2.1、定义路由
定义路由的三个细节
- 路由对应的URL必须以
/开头 app.url_map获取所有路由规则- 路由规则中主要包含 URL资源段、支持的请求方式、视图函数标记 三部分内容
app.route()的methods参数 指定路由支持的请求方式
from flask import Flask app = Flask(__name__) # 1.路由对应的URL必须以/开头 # 2.通过app的url_map属性获取所有的路由规则 (URL资源段 支持的请求方式 视图函数标记) # 3.可以通过route方法的methods参数指定路由支持的请求方式 @app.route('/hello', methods=['post', 'get']) def index(): return "index" if __name__ == '__main__': print(app.url_map) # 获取路由信息 # for rule in app.url_map.iter_rules(): # print(rule.rule, rule.methods, rule.endpoint) app.run(debug=True)
2.2、路由变量
- 路由变量的作用是 传递URL路径参数, 实现动态URL
- 格式:
/xx/<路由变量>
from flask import Flask app = Flask(__name__) # 路由变量: 传递URL路径参数 # 格式: /user/<路由变量名> @app.route('/user/<userid>') def index(userid): # 必须定义同名形参接收路由变量的值 print(userid) return "index" if __name__ == '__main__': app.run(debug=True)
2.3、路由转换器
- 路由转换器的作用是 对URL传递的参数进行格式校验, 类似Django设置URL时的正则表达式参数
- 格式:
/xx/<转换器名:路由变量> - 所有的转换器类都继承自 BaseConverter 类
from flask import Flask app = Flask(__name__) # 路由转换器: 对路由变量进行格式校验 条件不满足返回404 # 格式: /user/<路由转换器名:路由变量> @app.route('/user/<int:userid>') # int: 内置转换器, 要求1-n个整数 def index(userid): print(userid) return "index" if __name__ == '__main__': app.run(debug=True)
2.4、自定义转换器
- 除了使用内置的变量转换器, 开发者还可以自定义转换器, 更加灵活的校验路由变量
- 使用自定义转换器的步骤
- 定义转换器类, 继承BaseConverter
- 设置regex属性 (正则匹配规则)
- 应用添加自定义转换器
# 自定义转换器: # 1.定义转换器类, 继承BaseConverter # 2.设置regex属性 (正则匹配规则) # 3.添加自定义转换器 from flask import Flask from werkzeug.routing import BaseConverter app = Flask(__name__) # 1.定义转换器类 class MobileConverter(BaseConverter): # 2.设置regex属性(匹配规则) regex = '1[3-9]\d{9}$' # 不要设置开头的^ # 3.添加自定义转换器 app.url_map.converters['mob'] = MobileConverter @app.route('/user/<mob:mobile>') def index(mobile): print(mobile) return "index" if __name__ == '__main__': # 获取所有的转换器 {转换器名: 转换器类} # print(app.url_map.converters) app.run(debug=True)
3、请求
flask的请求数据是通过request对象来获取
常用属性:
| 属性 | 说明 | 类型 |
| url | 记录请求的URL地址 | str |
method |
记录请求使用的HTTP方法 |
str |
| headers | 记录请求中的报文头 | EnvironHeaders 类字典对象 |
args |
记录请求中的查询参数 |
MultiDict |
| form | 记录请求中的表单数据 | MultiDict |
data |
记录请求的数据,并转换为字符串 |
bytes |
| json | 记录请求体中的json数据 | Dict |
| files | 记录请求上传的文件 | MultiDict[str: FileStorage] |
from flask import Flask, request from werkzeug.datastructures import FileStorage app = Flask(__name__) @app.route('/', methods=['get', 'post']) def index(): # 获取请求的基础数据 # print(request.url) # 请求的URL # print(request.method) # 本次请求的请求方式 # print(request.headers) # 获取请求头信息 类字典对象 # print(request.headers['Host']) # print(request.headers.get('Host')) # 建议使用get方法, 键不存在不报错 # 请求传递数据 1> URL路径 -> 路由变量 2> 查询字符串 get 3> 请求体 post 4> 请求头 -> request.headers # 获取查询字符串 -> request.args xx?name=zs&age=20 类字典对象 # print(request.args.get('name')) # print(request.args.get('age')) # 请求体: 键值对(表单) 文本(json/xml) 文件(图片/音频) # 获取post键值对 -> request.form 类字典对象 # print(request.form.get('username')) # 获取post文本数据 -> request.data / request.json # print(request.data) # 返回bytes类型 # print(request.json.get('age')) # request.json直接将json字符串转为字典 # 获取post文件 -> request.files 类字典对象 file = request.files.get("avatar") # type: FileStorage # print(type(file)) # 返回 FileStorage文件对象 # 将文件保存到本地 file.save('123.jpg') # 获取文件的二进制数据 # img_bytes = file.read() # print(img_bytes) return "index" if __name__ == '__main__': app.run(debug=True)
4、响应
4.1、访问静态资源
- 将静态资源放入到 项目的
static文件夹中 - 通过内置的静态资源的访问路由, URL路径格式为
/static/<filename>- 如
static目录放入文件123.jpg, 则访问URL为http://127.0.0.1:5000/static/123.jpg
- 如
- Flask对象的初始化参数 也可以 修改静态资源的存储和访问路径
from flask import Flask app = Flask(__name__, # 导入名称, flask会根据该参数查询静态文件的存储路径 # 官方建议直接使用__name__, 表示从当前目录中查询静态文件存储路径 static_folder="static1", # 设置静态文件的存储目录 static_url_path='/res/img', # 设置静态文件的URL访问路径 如 127.0.0.1:5000/res/img/123.jpg ) if __name__ == '__main__': app.run(debug=True)
4.2、设置响应数据
Flask中设置响应数据主要有两种方式:
- 设置多个返回值
- 自定义响应对象
①、三个返回值
Flask中 视图函数的返回值可以设置三个, 分别对应 响应体, 响应状态码, 响应头
②、自定义响应对象
- 视图函数返回的
str / bytes类型数据会被包装为Response响应对象, 也可以 创建响应对象来 自定义响应头 等信息
from flask import Flask, make_response, Response app = Flask(__name__) # 自定义响应对象 @app.route('/demo2') def demo2(): # 视图函数的返回值可以为str/bytes类型, 并且flask内部会将其包装为Response响应对象 # return 'hello flask' # 创建响应对象 设置响应头时,需要手动创建响应对象 response = make_response('hello flask') # type: Response # 设置响应头 response.headers['B'] = 10 return response if __name__ == '__main__': app.run(debug=True)
4.3、返回json
- 如果接口需要返回
JSON数据,在 Flask 中可以直接使用jsonify()生成一个 JSON 的响应 - 不推荐使用
json.dumps()直接返回,因为返回的数据要符合 HTTP 协议规范,如果是JSON需要指定content-type:application/json
from flask import Flask, make_response, Response, jsonify app = Flask(__name__) @app.route('/demo3') def demo3(): dict1 = {'name': 'zs', 'age': 20} # 字典转json字符串 # return json.dumps(dict1) # 可以将字典转json字符串, 并且设置响应头的content-type为application/json # return jsonify(dict1) return jsonify(name='zs', age=20) # 也支持关键字实参的形式 if __name__ == '__main__': app.run(debug=True)
4.4、重定向
flask中通过 redirect() 实现重定向功能
from flask import Flask, redirect app = Flask(__name__) @app.route('/demo3') def demo3(): return jsonify(name='zs', age=20) @app.route('/demo4') def demo4(): # 重定向到指定网站 # return redirect('http://www.baidu.com') # 重定向到自己的路由 只需要URL资源段 return redirect('/demo3') if __name__ == '__main__': app.run(debug=True)
4.5、状态保持
- 1. Cookie
- 2. Session
- 3. jwt
①、cookie
特点:
- 将数据保存在 客户端 (用户的电脑上), 可以减轻服务器压力
- 访问网站时, 浏览器会 自动 将该网站的cookie数据发送给服务器
使用场景:
- 保存一些 不太重要的数据
from flask import Flask, make_response, Response, request app = Flask(__name__) @app.route('/') def index(): # 后端设置cookie: 通过响应体的set_cookie字段 # 创建响应对象 response = make_response('index') # type: Response # 设置响应头的set_cookie字段 value必须是str/bytes类型 response.set_cookie('per_page', '10', max_age=86400) # 删除cookie 本质: 设置max-age=0 # response.delete_cookie('per_page') # 返回响应对象 return response @app.route('/demo1') def demo1(): # 获取cookie: 浏览器会自动通过请求头的cookie字段来传递cookie数据 # request.cookies 直接获取到字典形式的cookie数据 print(request.cookies.get('per_page')) return 'demo1' if __name__ == '__main__': app.run(debug=True)
②、session
特点:
- 将数据保存在 服务端 (服务器的数据库中), 安全性更高
使用场景:
- 保存一些 重要/敏感的数据
from datetime import timedelta from flask import Flask, session app = Flask(__name__) # 设置应用秘钥 会被用于session签名 app.secret_key = 'test' # 设置session过期时间 默认31天 app.permanent_session_lifetime = timedelta(days=7) @app.route('/') def index(): # session是一个类字典对象, 对其取值/赋值 就可以实现session数据的读写 # 记录session数据 session['username'] = 'zs' # 设置session支持过期时间 session.permanent = True # 删除session数据 # session.pop('username') return "index" @app.route('/demo1') def demo1(): # 获取session数据 name = session.get('username') print(name) return 'demo1' if __name__ == '__main__': app.run(debug=True)
flask的默认session机制没有将session数据保存到服务器的数据库中, 而是将session数据编码后保存到了cookie中 (签名cookie机制)可以使用
flask-session组件 来实现传统的session存储
③、jwt
from datetime import datetime, timedelta from flask import Flask, request import jwt app = Flask(__name__) """ 需求:使用jwt-token实现状态保持 —— 支持HTML iOS 安卓 提前:pip install pyjwt 生成token:jwt.encode() 校验token:jwt.decode() """ @app.route("/login") def login(): """ 登录成功生成一个2小时有效的token :return: """ # 准备过期时长-格林威治0时区的时间戳 ctime = datetime.utcnow() time_d = timedelta(hours=2) expire = ctime + time_d # 准备载荷 payload = { "user_id": 6, "user_name": "yangwenjin", "exp": expire } # 准备密钥 key = "python39" # 生成2小时有效的token token = jwt.encode(payload=payload, key=key, algorithm='HS256') print(token) # 将token转换成字符串 token = token.decode() print(token) return token @app.route("/index") def index(): # 获取token token = request.headers.get("token") # 校验token try: payload = jwt.decode(token, key="python39", algorithms=['HS256']) except Exception as e: print(e) payload = None # 从载荷提取用户信息 if payload: user_id = payload.get("user_id") user_name = payload.get("user_name") return "用户信息为:user_id: {}, user_name: {}".format(user_id, user_name) else: return "invalid token", 401 if __name__ == '__main__': app.run(debug=True, host="0.0.0.0", port=5000)
4.6、异常处理
flask对 HTTP错误 进行了封装, 可以捕获http错误, 也可以主动抛出http错误
from flask import Flask, abort # flask对http错误进行了封装, 可以捕获http错误也可以主动抛出http错误 app = Flask(__name__) # 捕获http错误 @app.errorhandler(404) def error_404(error): # 一旦进行捕获, 要求必须定义形参接收具体错误信息 return "<h3>您访问的页面去浪迹天涯了</h3> %s" % error # 还可以捕获系统内置错误 @app.errorhandler(ZeroDivisionError) def error_zero(error): return '除数不能为0' @app.route('/') def index(): # a = 1 / 0 abort(500) # 主动抛出异常 (只能抛出http错误) return "index" if __name__ == '__main__': app.run(debug=True)
三、高级处理
1、请求钩子
- 请求钩子可以对请求的各阶段进行监听, 方便开发者 针对请求完成一些统一的处理, 以便减少重复代码, 作用类比Django中的中间件
- 开发中中主要会用到以下四种请求钩子:
- before_request
- 每次执行视图函数之前调用
- 对请求进行一些准备处理
- 如果在该函数中返回了一个响应,视图函数将不再被调用
- after_request
- 如果没有抛出错误,每次执行视图函数之后(已经包装为响应对象)调用
- 在此函数中可以对响应值在返回之前做最后一步修改处理
- 接受一个参数:包装好的响应对象
- 需要将修改后的响应对象返回
- before_first_request
- web应用被第一次请求前调用
- 可以进行web应用初始化处理
- teardown_request:
- 每次执行视图函数之后调用
- 无论是否出现异常都会执行, 一般用于请求收尾
- 接受一个参数:错误信息,如果有相关错误抛出
- before_request
from flask import Flask, Response # 请求钩子: 类比django中间件 ,请求钩子可以对请求的各阶段进行监听, 方便开发者针对请求完成一些统一的处理 app = Flask(__name__) # 每次执行视图函数之前调用, 对请求进行一些准备处理, 如参数解析, 黑名单过滤, 数据统计等 @app.before_request def prepare(): print('before_request') # 每次执行视图函数之后(已经包装为响应对象)调用, 对响应进行一些加工处理, 如设置统一响应头, 设置数据的外层包装 @app.after_request def process(response:Response): # 必须定义形参接收响应对象 print('after_request:') # print(response.headers) # print(response.data) # print(response.status_code) return response # web应用被第一次请求前调用, 可以进行web应用初始化处理, 如数据库连接 @app.before_first_request def initial(): print('before_first_request') # 每次执行视图函数之后调用, 无论是否出现异常都会执行, 一般用于请求收尾, 如资源回收, 异常统计 @app.teardown_request # 测试时不要开启调试模式 def request_handle(error): # 必须定义形参来接收具体错误信息, 如果没有错误, error=None print('teardown_request : %s' % error) @app.route('/') def index(): print('执行视图') a = 1 / 0 return "index" if __name__ == '__main__': app.run()
- 请求钩子可以通过两种方式进行添加, 装饰器形式 或者 直接方法调用
from flask import Flask, Response app = Flask(__name__) # 每次执行视图函数之前调用 # @app.before_request # def prepare(): # print('before_request') # 另一种语法 def prepare(): print('before_request') app.before_request(prepare) @app.route('/') def index(): print('执行视图') a = 1 / 0 return "index" if __name__ == '__main__': app.run()
2、蓝图
2.1、基本使用
- 蓝图的作用: 实现Flask项目 模块化
- 项目模块化主要是 将业务与功能模块进行划分, 每个功能模块对应一个包, 用于存放和其有关的视图/工具/模型文件等, 如
home,user - 对于大型项目, 一般 每个功能模块对应创建一个蓝图, 由多个蓝图代替应用来分别管理各模块的视图
--------- project # 工程目录 |------ main.py # 启动文件 |------ user # 用户模块 | |--- __init__.py # 包的初始化文件, 此处创建管理用户模块的蓝图对象 | |--- views.py # 视图文件 | |--- ... | |------ home # 首页模块 | |--- __init__.py # 包的初始化文件, 此处创建管理首页模块的蓝图对象 | |--- views.py # 视图文件 | |--- ... |...
- 在
home包 的初始化文件__init__.py中, 创建蓝图对象
# home/__init__.py from flask import Blueprint # 1. 创建蓝图对象 home_blu = Blueprint("home_b", __name__)
- 在
home包中创建views文件, 存放视图函数 - 定义视图函数时, 使用蓝图对象来定义路由
# home/views.py from home import home_blu # 2. 使用蓝图对象来定义路由 @home_blu.route('/') def index(): return "index"
- 想要让蓝图对象能够完成路由定义, 还需要 Flask应用注册蓝图对象
# main.py from flask import Flask from home import home_blu app = Flask(__name__) # 3.应用注册蓝图对象 app.register_blueprint(home_blu) if __name__ == '__main__': print(app.url_map) app.run(debug=True)
- 视图文件目前是独立的, 还需要 包的初始化文件导入视图文件, 让视图文件和主程序建立关联
# home/__init__.py from flask import Blueprint # 1. 创建蓝图对象 home_blu = Blueprint("home_b", __name__) # 4. 让视图文件和主程序建立关联 # 遇到ImportError, 需要查看和调整代码的执行顺序 from . import views
2.2、使用细节
蓝图的三个使用细节
- 创建蓝图时, 可以通过
url_prefix参数 给蓝图定义的路由添加 统一的URL资源段前缀 - 蓝图定义的路由, 其函数标记为 蓝图名.函数名
- 蓝图也可以 设置请求钩子
- 只有访问该蓝图定义的路由时才会触发
- 实现局部监听
# home/__init__.py from flask import Blueprint # 细节1: 可以通过url_prefix参数给蓝图定义的路由添加统一的URL资源段前缀 home_blu = Blueprint("home_b", __name__, url_prefix='/home') # 细节3: 蓝图也可以设置请求钩子 只有访问该蓝图定义的路由时才会触发 局部监听 @home_blu.before_request def home_prepare(): print('home_prepare') from . import views
# home/views.py from flask import url_for from home import home_blu @home_blu.route('/') def index(): return "index" @home_blu.route('/demo1') def demo1(): # 细节2: 蓝图定义的路由, 其函数标记为 蓝图名.函数名 url1 = url_for('home_b.demo1') # print(url1) return 'demo1'
3、上下文
- 上下文:是一个 数据容器,保存了 Flask 程序运行过程中的一些信息。
- Flask中有两种上下文,请求上下文 和 应用上下文
- 两种上下文的使用范围相同, 生命周期(从请求开始到请求结束), 在范围外使用会报错
3.1、请求上下文
- 记录一些和请求有关的数据, 包括request和session两个变量
- request
- 封装了HTTP请求的内容,针对的是http请求。
- session
- 用来记录请求会话中的信息,针对的是用户信息。
3.2、应用上下文
- 记录一些和应用有关的数据, 包括current_app和g两个变量
- current_app
- 会自动引用创建的Flask对象, 需要在项目的其他文件中使用app时, 应该通过current_app来获取, 可以减少循环导入问题
- g
- flask给开发者预留的一个容器, 用于记录自定义数据
- g变量每次请求会重置数据
- g使用场景:
- 1、在钩子函数和视图函数之间传递数据
- 2、函数嵌套调用时传递数据
# main.py from flask import Flask, request, current_app, g # 上下文变量: 有使用范围 [请求开始, 请求结束] # 请求上下文: 记录一些和请求有关的数据 request session # 应用上下文: 记录一些和应用有关的数据 current_app g import tool app = Flask(__name__) @app.route('/') def index(): # print(request.url) g.name = 'zs' tool.func1() return "index" @app.route('/demo1') def demo1(): print(g.name) # 会报错 return 'demo1' if __name__ == '__main__': # print(request.url) # 使用范围外, 会报错 app.run(debug=True)
# tool.py from flask import g, current_app def func1(): print(g.name) print(current_app.url_map) # tool2.func2()
3.3、上下文机制理解
问题1、上下文变量是否为全局变量
不是全局变量, web服务器会通过多线程并发调用web应用, 而全局变量会被所有线程共享, 无法记录并发的多个请求数据
上下文机制实现了线程隔离(LocalStack类型, 本质是字典, key是线程id, 值是上下文变量), 每个线程存取自己的数据, 相互不影响
- 请求1 -> 线程1 -> request = 请求1
- 请求2 -> 线程2 -> request = 请求2
问题2、上下文为什么设置使用范围
- 主要目的为节省内存
- 请求开始时, 创建上下文(记录上下文变量);
- 请求结束时, 销毁上下文(将上下文变量删除, 数据占用的空间被释放)
4、综合认证
4.1、统一处理
- 需求: 获取用户身份
- 分析: 除了静态资源, 基本所有视图都需要获取用户身份, 每个视图单独获取出现大量的代码冗余
- 解决办法: 设置 请求钩子, 并通过 g变量 将数据传递给视图函数
from flask import Flask, session, g app = Flask(__name__) app.secret_key = 'test' # 需求1: 所有视图都需要获取用户身份 # 解决办法: 用钩子函数进行封装 减少代码冗余 @app.before_request def prepare(): # 必须使用g变量来传递数据, 使用全局变量不能记录并发的多个请求数据 g.name = session.get('username') @app.route('/') def index(): if g.name: return "欢迎回来, %s" % g.name else: return '首页' @app.route('/demo1/') def demo1(): print(g.name) return 'demo1' @app.route('/login') def login(): """登录""" session['username'] = 'zs' return '登录成功' if __name__ == '__main__': app.run(debug=True)
4.2、访问限制
- 需求: 对指定的路由进行访问限制
- 分析: 部分视图需要身份校验, 这部分视图每个单独校验仍会出现大量的代码冗余
- 解决办法: 封装 装饰器 完成身份校验逻辑, 对指定视图函数设置装饰器
from flask import Flask, session, g, abort from functools import wraps app = Flask(__name__) app.secret_key = 'test' @app.before_request def prepare(): g.name = session.get('username') @app.route('/') def index(): if g.name: return "欢迎回来, %s" % g.name else: return '首页' @app.route('/login') def login(): """登录""" session['username'] = 'zs' return '登录成功' # 需求2: 对部分视图进行访问限制 如个人中心必须登录才能访问 # 解决方案: 使用装饰器封装访问限制 减少代码冗余 def login_required(f): # f = user def wrapper(*args, **kwargs): # 获取函数名 print(wrapper.__name__) if g.name: # 用户已登录 return f(*args, **kwargs) # 正常访问视图函数 else: # 用户未登录 abort(401) # 400 语法/参数错误 401 未认证 403 已认证, 权限不足 404 资源不存在 405 请求方式不支持 500 服务器错误 return wrapper @app.route('/user') @login_required # user = login_required(user) def user(): """个人中心""" return '访问 %s 的个人中心' % g.name if __name__ == '__main__': print(app.url_map) app.run(debug=True)
functools.wraps
- 系统内置的装饰器, 主要用于装饰器中的闭包函数
- 作用是 将被装饰的函数(wrapper)的函数信息 替换为 指定函数(f)的函数信息 (包括name 函数名, doc 函数注释等)
- 未设置wraps装饰器的情况:
def decorater1(f): # f = demo def wrapper(*args, **kwargs): # 获取函数名 print(wrapper.__name__) return f(*args, **kwargs) return wrapper @decorater1 def demo(): print("哈哈") # 调用函数 demo()
# 运行结果 wrapper # 打印闭包函数名 哈哈
- 设置wraps装饰器的情况:
from functools import wraps def decorater1(f): # f = demo @wraps(f) # 设置装饰器 def wrapper(*args, **kwargs): # 获取函数名 print(wrapper.__name__) return f(*args, **kwargs) return wrapper @decorater1 def demo(): print("哈哈") # 调用函数 demo()
# 运行结果 demo # 打印原函数名 哈哈
- flask中的函数标记是根据函数名生成的
- 视图函数添加装饰器
login_required后, 函数标记都会使用闭包函数名wrapper, 这样会出现函数标记冲突, 程序报错 - 解决办法: 给闭包函数添加装饰器
functools.wraps(f), 让函数标记使用原视图函数名生成
from flask import Flask, session, g, abort from functools import wraps app = Flask(__name__) app.secret_key = 'test' @app.before_request def prepare(): g.name = session.get('username') @app.route('/') def index(): if g.name: return "欢迎回来, %s" % g.name else: return '首页' @app.route('/login') def login(): """登录""" session['username'] = 'zs' return '登录成功' # 使用装饰器封装访问限制 def login_required(f): # f = user @wraps(f) # 会将被装饰的函数(wrapper)的函数信息替换为指定函数(f)的函数信息(__name__ 函数名, __doc__ 函数注释) # 设置该装饰器后, 可以让闭包函数使用原函数名, 避免函数标记出现冲突(函数标记是根据函数名来生成的) def wrapper(*args, **kwargs): if g.name: return f(*args, **kwargs) else: abort(401) return wrapper @app.route('/user') @login_required def user(): """个人中心""" return '访问 %s 的个人中心' % g.name @app.route('/demo1') @login_required def demo1(): return 'demo1' if __name__ == '__main__': print(app.url_map) app.run(debug=True)
5、应用配置
5.1、加载配置
app.config 用于设置配置, 该属性继承自 dict, 可以以字典形式赋值取值
from datetime import timedelta from flask import Flask, session app = Flask(__name__) app.secret_key = 'test' # config属性用于设置配置 app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) @app.route('/') def index(): # 设置session 用于测试配置是否生效 session['name'] = 'zs' # 读取配置 print(app.config.get('PERMANENT_SESSION_LIFETIME')) return "index" if __name__ == '__main__': app.run(debug=True)
- 实际开发中, 应该将配置封装起来, 方便进行统一的管理, 类似Django的settings文件
- Flask提供了多种封装方式, 这里先介绍一种常用的方案 -- 从对象中加载配置
从对象中加载配置
- 实际开发中, 项目往往存在多套配置
- 开发环境配置
- 生产环境配置
- 测试环境配置
- ...
- 各种环境的配置既有相同也有不同, 大量相同的配置会出现代码冗余
- 从对象中加载配置
- 配置封装方案之一, 以面向对象的形式 封装配置, 有利于 减少重复代码 以及 代码解耦合
代码示例:
- 定义配置文件
config.py, 在文件中将应用配置 以类的形式 封装起来
# config.py from datetime import timedelta class BaseConfig: """配置基类 可以将相同的配置抽取到基类中, 减少重复代码""" # 定义和配置同名的类属性 PERMANENT_SESSION_LIFETIME = timedelta(days=7) class DevelopmentConfig(BaseConfig): """开发环境""" SQL_URL = '127.0.0.1:3306/test1' # 数据库地址 class ProductionConfig(BaseConfig): """生产环境""" SQL_URL = '222.10.15:3306/users' # 数据库地址
- 主文件
main.py从对象中加载封装的配置app.config.from_object()
# main.py from datetime import timedelta from flask import Flask app = Flask(__name__) # 从对象中加载配置 # 优点: 面向对象的设计有利于 减少重复代码 以及 代码解耦合 from config import DevelopmentConfig app.config.from_object(DevelopmentConfig) @app.route('/') def index(): print(app.config.get('PERMANENT_SESSION_LIFETIME')) return "index" if __name__ == '__main__': app.run(debug=True)
5.2、切换配置
- 虽然封装了多套配置, 但需要 修改代码才能切换配置, 这种方式并不利于开发和测试
- Flask提供了切换配置的更好方案, 需要进行以下两步:
- 定义工厂函数, 封装应用的创建过程
- 利用环境变量, 调用工厂函数, 指定配置并动态创建应用
5.2.1、定义工厂函数
定义工厂函数, 封装应用的创建过程
在前一节 config.py 的基础上, 定义字典记录配置类型和配置子类之间的映射关系
# config.py from datetime import timedelta class BaseConfig: """配置基类 可以将相同的配置抽取到基类中, 减少重复代码""" # 定义和配置同名的类属性 PERMANENT_SESSION_LIFETIME = timedelta(days=7) class DevelopmentConfig(BaseConfig): """开发环境""" SQL_URL = '127.0.0.1:3306/test1' # 数据库地址 class ProductionConfig(BaseConfig): """生产环境""" SQL_URL = '222.10.15:3306/users' # 数据库地址 # 定义字典来记录 配置类型 和 配置子类 之间的映射关系 config_dict = { 'dev': DevelopmentConfig, 'pro': ProductionConfig }
在主文件 main.py 中, 定义工厂函数封装应用的创建过程, 并通过参数指定应用对应的配置类型
from flask import Flask, current_app, Config from config import config_dict # 工厂函数: 根据参数需求, 内部封装对象的创建过程 def create_app(config_type): """封装应用的创建过程""" # 创建应用 flask_app = Flask(__name__) # 根据配置类型取出对应的配置子类 config_class = config_dict[config_type] # 加载普通配置 flask_app.config.from_object(config_class) return flask_app # 创建应用对象 app = create_app('dev') @app.route("/") def index(): print(app.config.get('SQL_URL')) return "index" if __name__ == '__main__': app.run()
5.2.2、加载隐私配置
- 对于线上项目, 隐私配置 直接写在项目的配置文件中, 会 增加数据泄露的风险
- FLASK提供了 从环境变量中加载配置 的方案, 可用于加载隐私配置
- 该方案允许 通过环境变量指定配置文件的路径, 且 路径允许在项目目录以外
- 环境变量格式:
export 环境变量名="隐私配置的文件路径"
代码示例:
- 在项目目录外 创建隐私配置文件
secret_config.py, 并以全局变量形式设置隐私配置
# secret_config.py SECRET_KEY = 'heima123' # 隐私配置
- 主文件
main.py通过环境变量方式来加载隐私配置
# main.py from datetime import timedelta from flask import Flask app = Flask(__name__) # 从环境变量中加载配置 # 优点: 可以保护隐私配置 export ENV_CONFIG="隐私配置的文件路径" app.config.from_envvar('ENV_CONFIG') @app.route('/') def index(): print(app.config.get('SECRET_KEY')) return "index" # if __name__ == '__main__': # app.run(debug=True)
- 环境变量&终端命令 启动程序
$ export FLASK_APP="main" # 设置内置环境变量 $ export ENV_CONFIG="/xx/secret_config.py" # 设置隐私配置对应的环境变量 $ flask run # 启动web程序
- 实际开发中的方案
- 开发阶段, 只加载普通配置
- 生产阶段, 先加载普通配置, 再通过环境变量的方式加载项目以外的隐私配置并覆盖原有配置
代码示例:
def create_app(config_type): """封装应用的创建过程""" # 创建应用 flask_app = Flask(__name__) # 根据配置类型取出对应的配置子类 config_class = config_dict[config_type] # 先加载普通配置 flask_app.config.from_object(config_class) # 再加载隐私配置 silent=True, 配置加载失败也不报错 flask_app.config.from_envvar('ENV_CONFIG', silent=True) return flask_app
- 加载配置时, 设置参数
silent=True, 则配置加载失败也不会报错
![flask[5] flask[5]](https://img2020.cnblogs.com/blog/2139038/202010/2139038-20201029182050418-745846456.png)
浙公网安备 33010602011771号