0. 前情提要:
-
掌握基础的flask语法知识与python语法知识。
-
参考资料:https://www.bilibili.com/video/BV11Y411h71J
0.1 flask版本
-
本文所学习的flask源码的版本为0.12,建议在虚拟环境中安装。
pip install flask==0.12
注意,我在研究代码并搜寻网上的源码解读文章时,发现文章中的源码和自己看到的源码是不一样的,并且我的最简单的
flask
程序是无法运行的,会报错cannot-import-name-markup-from-jinja2
。经查询,StackOverflow给了我答案,
flask
与其依赖包jinja2
的版本不匹配。只是简单的通过
pip install flask==0.12
下载0.12版本的flask
是不行的,其依赖的werkzeug
和jinja2
的仍会自动下载最新版本的,故而这里需要给其相关依赖包降级。我在网上找到0.12版本的
flask
适配的依赖包版本,按照下面的版本给依赖包降级即可。据本人实操,给werkzeug
,Jinj2
,itsdangerous
,MarkupSafe
降级即可使用,且源码与网上搜到的基本保持一致了。降级的方法:
Pycharm---->File---->Settings---->Project---->Python Interpreter----> + ---->搜索框中输入Werkzeug---->降级
0.2 查看源码
-
源码跳转:按住
ctrl
后,单击需要查看源码的函数或类对象即可 -
源码回退:
Alt+Ctrl +方向左键
或者,选中Pycharm---->View---->Appearance---->Toolbar
,之后点击左上角的箭头即可
- 源码部分会删除注释及一些不相干部分,只讲重点代码。
0.3 flask程序
本文针对下列简单的flask程序讨论源码
- 项目目录
hello文件夹
- settings
- config.py 【配置文件】
- static
- login.html
- templates
- login.html
- index.html
- hello.py
- flask程序
from flask import Flask, request
app = Flask(__name__)
app.config.from_object("settings.config")
@app.before_request
def f1():
pass
@app.before_request
def f2():
pass
@app.after_request
def f3(response):
return response
@app.after_request
def f4(response):
return response
@app.before_first_request
def f5():
pass
@app.before_first_request
def f6():
pass
@app.route('/hello')
def hello():
return 'Hello, Flask!'
if __name__ == "__main__":
app.run()
1. Flask创建app对象
1.1 初识Flask对象
app = Flask(__name__)
# 1. 实例化一个Flask对象
# 2. __name__是一种魔法属性,即当前模块(py文件)的名字
# 【注:若当前模块为启动模块,则__name__ = "__main__"】
# 3. 该参数是为了指明flask的根目录,默认在该目录下的static中寻找静态资源,templates目录中寻找模板资源
# 【静态资源可通过.../static/index.html访问,此时不为url,而是静态目录】
# 4. 若输入的模块不存在,则默认在当前py文件的根目录寻找资源
# 5. 注意不要随意输入,例如abc模块时存在的
1.2 初始化app = Flask(__name__)
按住ctrl
后,单击app = Flask(__name__)
中的Flask
,查看Flask类的初始化函数__init__
。
# Flask.__init__
def __init__(self,
import_name,
static_path=None,
static_url_path=None,
static_folder='static',
template_folder='templates',
instance_path=None,
instance_relative_config=False,
root_path=None):
....
首先了解一下参数的含义:
常用参数
-
import_name
:flask的以该模块所在目录为根目录,默认在该目录下的static
中寻找静态资源,templates
目录中寻找模板资源 -
static_url_path
: 访问静态资源的url前缀,默认值是static
。假设有静态资源apple.png
,默认情况下输入127.0.0.1:5000/static/apple.png
,即可访问该静态资源。app = Flask(__name__, static_url_path="/python") # 则访问127.0.0.1/python/apple.png,即可访问该静态资源
-
static_folder
:存放静态资源的目录,默认为static。注意与
static_url_path
作区分,我们考虑这么一个问题,当访问/login.html
时,我们究竟访问的是静态资源,还是已经绑定了视图函数的url呢?# 1.静态资源 - hello文件夹 - static文件夹 -login.html 【静态资源】 # 2.绑定了视图函数的url @app.route('/login.html') def login(): return "hello login"
所以flask将
static_url_path
作为访问静态资源的url前缀,当访问url的前缀为设定的static_url_path
值时,认为访问的是静态资源。即默认情况下,
访问
/static/login.html
,则返回静态资源login.html
。访问
/login.html
,则认为访问的是绑定了视图函数的url,执行其视图函数login
。 -
template_folder
:存放模板资源的 目录,默认为templates。
其他参数
instance_path
:目前不理解这个参数的意义,详情参考
代码分析
# Flask.__init__
def __init__(self,
import_name,
static_path=None,
static_url_path=None,
static_folder='static',
template_folder='templates',
instance_path=None,
instance_relative_config=False,
root_path=None):
"""将相关参数封装到app中"""
# 调用基类的初始化函数将import_name,template_folder,rootpath封装到app中
_PackageBoundObject.__init__(self, import_name,
template_folder=template_folder,
root_path=root_path)
# 将static_url_path, static_folder参数等封装到app中
self.static_url_path = static_url_path
self.static_folder = static_folder
...
"""一些重要的实例属性"""
# 加载配置文件,其详细内容在下一部分会介绍
self.config = self.make_config(instance_relative_config)
# 用于存放视图函数和endpoint的对应关系,endpoint为键,视图函数为值
self.view_functions = {}
# 用于存放before_request函数,即视图函数执行前执行的函数。内部格式为{None:[f1,f2]}
self.before_request_funcs = {}
# 用于存放before_first_request函数,即第一次接收请求时视图函数执行前执行的函数。内部格式为[f5,f6]
self.before_first_request_funcs = []
# 用于存放after_request函数,即视图函数执行后执行的函数。内部各式为{None:[f3,f4]}
self.after_request_funcs = {}
# 用于存放Rule对象,Rule对象内部封装了url,endpoint,methods等属性
self.url_map = Map()
# 是否为已经获取第一次请求的标识,默认为False值,即还未接收第一次请求。当接收过第一次请求后会设为True
self._got_first_request = False
"""加载静态资源的路由,通过这一路由可以访问静态资源"""
if self.has_static_folder:
self.add_url_rule(self.static_url_path + '/<path:filename>',
endpoint='static',
view_func=self.send_static_file)
-
调用
_PackageBoundObject.__init
__函数# 此时的self是Flask的实例化对象app def __init__(self, import_name, template_folder=None, root_path=None): self.import_name = import_name self.template_folder = template_folder if root_path is None: # 获取该模块的目录,在该目录下的static中寻找静态资源,templates中寻找模板资源 root_path = get_root_path(self.import_name) #: Where is the app root located? self.root_path = root_path self._static_folder = None self._static_url_path = None
-
Flask类属性
url_rule_class = Rule config_class = Config
-
app实例属性
self.属性 = value self.import_name self.static_url_path self.static_folder self.templates_folder self.config = make_config(instance_relative_config) self.view_functions = {} # 视图函数和endpoint的对应关系 self.error_handlers = {} self.before_first_request_funcs = [] self.before_request_funcs = [] # 中间件【请求前执行的函数】 self.after_request_funcs = [] # 中间件【请求后执行的函数】 self.url_map = Map() # 在其中存放url与endpoint的对应关系
-
添加静态资源的路由
self.add_url_rule(self.static_url_path + '/<path:filename>', endpoint='static', view_func=self.send_static_file) # 根据当前文件的目录结构【参考前情提要】 # url为/static/login.html,<path:filename>是转换器 # endpoint为'static' # 执行视图函数:app.send_static_file
2. 加载配置文件
from flask import Flask
app = Flask(__name__)
app.config.from_object("settings.config")
app.config
属性
在上面提及的__init__
函数中初始化了app.config
值,我们来看一看
# Flask.__init__函数
self.config = self.make_config(instance_relative_config)
目光转到make_config函数
def make_config(self, instance_relative=False):
# 省略了对root_path的一些处理,root_path一般是app的根目录
root_path = self.root_path
return self.config_class(root_path, self.default_config)
Flask
类中定义了
# Flask
config_class = Config
即make_config函数
创建并返回了一个Config
类对象。self.config
是一个Config
类。下面来看Config
类
Config
类
class Config(dict):
# 初始化函数
def __init__(self, root_path, defaults=None):
# 调用字典类型的初始化函数,将其设为空字典{}或指定的值default
dict.__init__(self, defaults or {})
# 记录app的根目录
self.root_path = root_path
# 读取配置文件
def from_object(self, obj):
# string_types = (str, unicode)
# 如果obj是str或unicode类型,则对其做一些处理,使其变为路径格式
if isinstance(obj, string_types):
# 对"setting.config"字符串做一些处理,得到模块的路径与名字,从而导入模块文件
obj = import_string(obj)
# dir():返回模块的属性列表
for key in dir(obj):
# 如果该属性变量名是大写的,将其属性变量名作为key,其属性值作为value,存入Config字典
if key.isupper():
# 执行__setattr__方法
# 继承了dict的__setattr__方法
self[key] = getattr(obj, key)
简单来说就是,Config
是一个字典。app.config.from_object("settings.config")
将配置文件settings.config
中的信息,以键值对的形式存入app.config
中。
3. 特殊装饰器
@app.before_request
def f1():
pass
@app.before_request
def f2():
pass
@app.after_request
def f3(response):
return response
@app.after_request
def f4(response):
return response
@app.before_first_request
def f5():
pass
@app.before_first_request
def f6():
pass
app.before_request
函数
def before_request(self, f):
# 将该函数加入到app.before_request_funcs字典中
# app.before_request_funcs={None:[f1,f2]}
self.before_request_funcs.setdefault(None, []).append(f)
return f
app.after_request
函数
def after_request(self, f):
# 将该函数加入到app.after_request_funcs字典中
# app.after_request_funcs={None:[f3,f4]}
self.after_request_funcs.setdefault(None, []).append(f)
return f
app.before_first_request
函数
def before_first_request(self, f):
# 将该函数加入到app.before_first_request_funcs列表中
# app.before_request_funcs=[f5,f6]
self.before_first_request_funcs.append(f)
return f
4. 配置路由
@app.route('/hello')
def hello():
return 'Hello, Flask!'
app.route
函数
def route(self, rule, **options):
# **关键词参数接收为options【字典dict类型】
# rule为"/hello"字符串
def decorator(f):
endpoint = options.pop('endpoint', None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
-
首先传入参数
rule='/hello'
,定义了decorator
函数,并返回该函数。 -
接着,将
decorator
函数作为hello
函数的装饰器。即执行decorator
函数,参数为hello
函数def decorator(f): endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f # hello_world函数名作为参数传入f # 执行dict类型的pop函数,pop(key[,default]),获取并获取删除中字典中的key对应元素,若关键字不存在,则返回设定的默认值default # 未传入关键字为endpoint的参数,故endpoint为默认值None
-
执行
add_url_rule
函数,看下面详解
add_url_rule
函数
我将它分为以下几个部分
-
传入参数:
rule='/hello'
,endpoint=None
,f=hello
-
处理参数
endpoint
:"""endpoint处理""" #如果endpoint为空,获取当前视图函数的名字,设为endpoint,将其放入options字典中 #即options={'endpoint': hello} if endpoint is None: # _endpoint_from_view_func函数逻辑:要求视图函数view_func非空,并返回它的名字 endpoint = _endpoint_from_view_func(view_func) options['endpoint'] = endpoint
-
处理参数
methods
:小知识点补充:
# a = b or c ,将b和c中不为None的值赋给a,如果b和c均不为None,则将前一个b赋值给a
代码:
"""methods处理""" # 将method方法从options字典中取出并删除,若不存在设为None methods = options.pop('methods', None) # 如果methods参数为空,获取view_func的methods属性,若其不存在,设为默认值'GET' if methods is None: methods = getattr(view_func, 'methods', None) or ('GET',) # methods参数要求是可迭代对象,一般是列表['GET','POST',...] if isinstance(methods, string_types): raise TypeError('Allowed methods have to be iterables of strings, ' 'for example: @app.route(..., methods=["POST"])') # methods中的参数要求大写 methods = set(item.upper() for item in methods) # 其他处理略
-
路由封装
"""将url和endpoint的对应关系封装到Rule对象,再将Rule对象封装到Map对象""" # rule = Rule(url, methods, endpoint) rule = self.url_rule_class(rule, methods=methods, **options) # 把rule对象封装到app.url_map中 self.url_map.add(rule) """绑定endpoint与视图函数""" # app.view_functions = {endpoint:view_func} # 即以endpoint为key,视图函数view_func为value存入app.view_functions字典中""" if view_func is not None: # 从view_functions的中获取endpoint对应的视图函数 old_func = self.view_functions.get(endpoint) # 如果endpoint已有对应的视图函数且与当前要绑定的视图函数,则报错 if old_func is not None and old_func != view_func: raise AssertionError('View function mapping is overwriting an ' 'existing endpoint function: %s' % endpoint) # 否则,以endpoint为key,视图函数view_func为value存入app.view_functions字典中 self.view_functions[endpoint] = view_func