接着上一篇flask是如何启动的,这篇主要讲路由映射是何时生成的,以及生成过程是如何的,如果有写的不太准确的地方,麻烦帮忙指正,谢谢,希望大家一起进步。
我们都知道,flask的路由匹配是基于python特有语法装饰器实现的,如下代码所示
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
if __name__ == '__main__':
app.run()
def route(self, rule, **options):
"""看这几个参数的解释就行了,其他的解释都是使用帮助"""
:param rule: the URL rule as string
:param methods: a list of methods this rule should be limited
to (``GET``, ``POST`` etc.). By default a rule
just listens for ``GET`` (and implicitly ``HEAD``).
:param subdomain: specifies the rule for the subdoain in case
subdomain matching is in use.
:param strict_slashes: can be used to disable the strict slashes
setting for this rule. See above.
:param options: other options to be forwarded to the underlying
:class:`~werkzeug.routing.Rule` object.
"""
def decorator(f):
self.add_url_rule(rule, f.__name__, **options)
self.view_functions[f.__name__] = f
return f
return decorator
0x00
要想看懂这个,得了解点装饰器相关的用法,这里简单介绍一下,这里主要是说针对函数的装饰器
首先大家必须要了解的是,装饰器函数是在被加载时就已经运行了,而不是等到__main__
函数确定调用到使用装饰器的地方才开始执行装饰器,举个例子说
python的脚本文件,大伙都知道,是从上往下运行的,碰到def或是class是不运行函数内容的,但是如果碰到@decrator这种,就会直接执行decroator(func)这个函数,当然func(*args)是不执行的
0x01
结构最简单的装饰器(不带参数)
# 最最常见的装饰器,作用是给被包装的函数加一些功能,比如计时啊,日志相关啊等等
import time
from functools import wraps
# 装饰器函数,参数为要被装饰的函数
def calc_time(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
# 执行原函数
result = func(*args, **kwargs)
end = time.time()
# 所添加的功能就是计算被装饰函数的运行时间
print(func.__name__, end-start)
return result
return wrapper
# 使用该装饰器,计算这个函数用时多久
@timethis
def countdown(n):
pass
# 上面的装饰器写法等同于下面的执行过程
def countdown(n):
pass
countdown = timethis(countdown)
0x02
和Flask.route相关的复杂一点的装饰器(带参数)
# 通过观察,可以看得出来,不带参数的装饰器函数只需要嵌套一层内部函数就行了
# 而带参数的装饰器函数,则需要嵌套两层,最外面一层是传递的参数,进行一些操作,第二层是传递被装饰的函数
# 第三层则是正常的被装饰函数的原有参数信息
from functools import wraps
import logging
# 外部参数在这里进行传递
def logged(level, name=None, message=None):
# 传递被装饰函数给内部装饰器
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
# 执行被装饰函数的地方
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
# 就可以传递参数,通过参数的判断,击行函数的执行
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
还是先看源码长什么样子
def add_url_rule(self, rule, endpoint, **options):
"""Connects a URL rule. Works exactly like the :meth:`route`
decorator but does not register the view function for the endpoint.
Basically this example::
@app.route('/')
def index():
pass
Is equivalent to the following::
def index():
pass
app.add_url_rule('index', '/')
app.view_functions['index'] = index
:param rule: the URL rule as string
:param endpoint: the endpoint for the registered URL rule. Flask
itself assumes the name of the view function as
endpoint
:param options: the options to be forwarded to the underlying
:class:`~werkzeug.routing.Rule` object
"""
options['endpoint'] = endpoint
options.setdefault('methods', ('GET',))
self.url_map.add(Rule(rule, **options))
注释解释的比较详细了,将装饰器和这个函数的关系解释的明明白白
这是Map的add方法,所以这里的self是指map对象,而for循环中的rule是Rule对象,所以rule.bind是要从Rule中寻找bind方法去研究,为什么这里说一下,因为Map类中也有个add方法,别看错了
def add(self, rulefactory):
"""Add a new rule or factory to the map and bind it. Requires that the
rule is not bound to another map.
:param rulefactory: a :class:`Rule` or :class:`RuleFactory`
"""
for rule in rulefactory.get_rules(self):
# self是指的当前的map对象
rule.bind(self)
self._rules.append(rule)
self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
self._remap = True
绑定路由到url_map函数
def bind(self, map, rebind=False):
"""Bind the url to a map and create a regular expression based on
the information from the rule itself and the defaults from the map.
:internal:
"""
if self.map is not None and not rebind:
raise RuntimeError("url rule %r already bound to map %r" % (self, self.map))
# 将所有规则全部放到一个map中
self.map = map
# 取一些必须的默然默认值
if self.strict_slashes is None:
self.strict_slashes = map.strict_slashes
if self.merge_slashes is None:
self.merge_slashes = map.merge_slashes
if self.subdomain is None:
self.subdomain = map.default_subdomain。
# 这个函数复杂,下面有
self.compile()
对路由进行规则的正则匹配,并生成最终的完整的路由映射
def compile(self):
"""Compiles the regular expression and stores it."""
assert self.map is not None, "rule not bound"
if self.map.host_matching:
domain_rule = self.host or ""
else:
domain_rule = self.subdomain or ""
self._trace = []
self._converters = {}
self._static_weights = []
self._argument_weights = []
regex_parts = []
def _build_regex(rule):
index = 0
# 转换器 (converter)、转换器参数(argument) 和名称(name)
for converter, arguments, variable in parse_rule(rule):
if converter is None:
for match in re.finditer(r"/+|[^/]+", variable):
part = match.group(0)
if part.startswith("/"):
if self.merge_slashes:
regex_parts.append(r"/+?")
self._trace.append((False, "/"))
else:
regex_parts.append(part)
self._trace.append((False, part))
continue
self._trace.append((False, part))
regex_parts.append(re.escape(part))
if part:
self._static_weights.append((index, -len(part)))
else:
if arguments:
c_args, c_kwargs = parse_converter_args(arguments)
else:
c_args = ()
c_kwargs = {}
convobj = self.get_converter(variable, converter, c_args, c_kwargs)
regex_parts.append("(?P<%s>%s)" % (variable, convobj.regex))
self._converters[variable] = convobj
self._trace.append((True, variable))
self._argument_weights.append(convobj.weight)
self.arguments.add(str(variable))
index = index + 1
_build_regex(domain_rule)
regex_parts.append("\\|")
self._trace.append((False, "|"))
_build_regex(self.rule if self.is_leaf else self.rule.rstrip("/"))
if not self.is_leaf:
self._trace.append((False, "/"))
self._build = self._compile_builder(False).__get__(self, None)
self._build_unknown = self._compile_builder(True).__get__(self, None)
if self.build_only:
return
if not (self.is_leaf and self.strict_slashes):
reps = u"*" if self.merge_slashes else u"?"
tail = u"(?/%s)" % reps
else:
tail = u""
regex = u"^%s%s$" % (u"".join(regex_parts), tail)
self._regex = re.compile(regex, re.UNICODE)
这个函数是将路由映射完成的核心函数,比较复杂,可以打断点走着看
def _compile_builder(self, append_unknown=True):
defaults = self.defaults or {}
dom_ops = []
url_ops = []
opl = dom_ops
for is_dynamic, data in self._trace:
if data == "|" and opl is dom_ops:
opl = url_ops
continue
# this seems like a silly case to ever come up but:
# if a default is given for a value that appears in the rule,
# resolve it to a constant ahead of time
if is_dynamic and data in defaults:
data = self._converters[data].to_url(defaults[data])
opl.append((False, data))
elif not is_dynamic:
opl.append(
(False, url_quote(to_bytes(data, self.map.charset), safe="/:|+"))
)
else:
opl.append((True, data))
def _convert(elem):
ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem))
ret.args = [ast.Name(str(elem), ast.Load())] # str for py2
return ret
def _parts(ops):
parts = [
_convert(elem) if is_dynamic else ast.Str(s=elem)
for is_dynamic, elem in ops
]
parts = parts or [ast.Str("")]
# constant fold
ret = [parts[0]]
for p in parts[1:]:
if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str):
ret[-1] = ast.Str(ret[-1].s + p.s)
else:
ret.append(p)
return ret
dom_parts = _parts(dom_ops)
url_parts = _parts(url_ops)
if not append_unknown:
body = []
else:
body = [_IF_KWARGS_URL_ENCODE_AST]
url_parts.extend(_URL_ENCODE_AST_NAMES)
def _join(parts):
if len(parts) == 1: # shortcut
return parts[0]
elif hasattr(ast, "JoinedStr"): # py36+
return ast.JoinedStr(parts)
else:
call = _prefix_names('"".join()')
call.args = [ast.Tuple(parts, ast.Load())]
return call
body.append(
ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
)
# str is necessary for python2
pargs = [
str(elem)
for is_dynamic, elem in dom_ops + url_ops
if is_dynamic and elem not in defaults
]
kargs = [str(k) for k in defaults]
func_ast = _prefix_names("def _(): pass")
func_ast.name = "" .format(self.rule)
if hasattr(ast, "arg"): # py3
func_ast.args.args.append(ast.arg(".self", None))
for arg in pargs + kargs:
func_ast.args.args.append(ast.arg(arg, None))
func_ast.args.kwarg = ast.arg(".kwargs", None)
else:
func_ast.args.args.append(ast.Name(".self", ast.Param()))
for arg in pargs + kargs:
func_ast.args.args.append(ast.Name(arg, ast.Param()))
func_ast.args.kwarg = ".kwargs"
for _ in kargs:
func_ast.args.defaults.append(ast.Str(""))
func_ast.body = body
# use `ast.parse` instead of `ast.Module` for better portability
# python3.8 changes the signature of `ast.Module`
module = ast.parse("")
module.body = [func_ast]
# mark everything as on line 1, offset 0
# less error-prone than `ast.fix_missing_locations`
# bad line numbers cause an assert to fail in debug builds
for node in ast.walk(module):
if "lineno" in node._attributes:
node.lineno = 1
if "col_offset" in node._attributes:
node.col_offset = 0
code = compile(module, "" , "exec")
return self._get_func_code(code, func_ast.name)
为了加深理解,建议用如下代码打断点,走一走我重点标注的这几个函数
from werkzeug.routing import Map, Rule
url_rules = [
Rule('/', endpoint='index', methods=['GET']),
Rule('/user', endpoint='user', methods=['GET'])
]
map = Map(url_rules)
print(map)
将url路由映射关系添加整理为小demo如下
from flask import Flask
from werkzeug.routing import Map, Rule
app = Flask(__name__)
def index():
return 'hello, world'
def user():
return 'hello, stranger'
# 假设映射关系是这样,需要自己构建
url_rules = [
Rule('/', endpoint='index', methods=['GET']),
Rule('/user', endpoint='user', methods=['GET'])
]
map_list = Map(url_rules)
# 将app和关联起来,其实就是Flask关于路由的属性需要设置
app.url_map = map_list
app.view_functions = {
'index': index,
'user': user
}
if __name__ == '__main__':
app.run()
到此结束,总结来看,从Flask
启动读到脚本中装饰器route
就开始就把把url
和响应的函数方法对应起来,从而应对客户端对于不同路由的响应