flask源码系列之路由Route

东门彬
2023-12-01


flask app例子,app是flask中的Flask类的实例对象。

# myflask.py
from flask import Flask
 
app = Flask(__name__)         #生成app实例
 
@app.route('/')
def index():
	return 'Hello World'

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

上述代码中的@app.route('/')就是flask中的路由功能的使用。route是app的方法,其实从用法也知道了,它实际是一个装饰器,装饰的是视图函数。举例说明,它的作用就是当浏览器发起http请求时,如果url中的path时’/‘的话,则调用index视图函数,也就是说将’/'这个path和index这个视图函数关联了起来。
为对应的视图函数指定URL,可以一对多,即一个函数对应多个URL。

由于Flask类中的许多实例方法都到了setupmethod来装饰,那么就先分析下这个装饰器吧。
flask中setupmethod函数的源码如下:

def setupmethod(f):
    """包裹一个方法使得改方法能够实现在debug模式下检查是否是第一次request请求,
    只有不是第一次请求才能够调用被装饰的函数。
    """
    def wrapper_func(self, *args, **kwargs):
        if self.debug and self._got_first_request:
            raise AssertionError('A setup function was called after the '
                'first request was handled.  This usually indicates a bug '
                'in the application where a module was not imported '
                'and decorators or other functionality was called too late.\n'
                'To fix this make sure to import all your view modules, '
                'database models and everything related at a central place '
                'before the application starts serving requests.')
        return f(self, *args, **kwargs)
    return update_wrapper(wrapper_func, f)

其中update_wrapper是python中functool包中的一个方法。他的作用是

更新一个包裹(wrapper)函数,使其看起来更像被包裹(wrapped)的函数。
可选的参数指定了被包裹函数的哪些属性直接赋值给包裹函数的对应属性,同时包裹函数的哪些属性要更新而不是直接接受被包裹函数的对应属性,参数assigned的默认值对应于模块级常量WRAPPER_ASSIGNMENTS(默认地将被包裹函数的 namemodule,和 doc 属性赋值给包裹函数),参数updated的默认值对应于模块级常量WRAPPER_UPDATES(默认更新wrapper函数的 dict 属性)。
这个函数的主要用途是在一个装饰器中,原函数会被装饰(包裹),装饰器函数会返回一个wrapper函数,如果装饰器返回的这个wrapper函数没有被更新,那么它的一些元数据更多的是反映wrapper函数定义的特征,无法反映wrapped函数的特性。
因为装饰之后print(被装饰函数.name)或print(被装饰函数.doc)会发现这些都是装饰器函数中的包裹函数的__name__和__doc__,而不是最初这个的函数的__name__和__doc__。
update_wrapper()的作用就是将__name__和__doc__改为最初那个被装饰的函数的__name__和__doc__。

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    """更新包装函数(wrappper)以使其看起来像被包装函数(wrapped)。
    :param wrapper: 需要更新的函数。
    :param wrappered: 最初的函数,也就是被包装函数。
    :param assigned: 是一个元组。作用是直接将wrappered函数的元组中的属性赋值给wrapper函数,默认值是functools.WRAPPER_ASSIGNMENTS。
    :param updated: 是一个元组。根据wrappered函数的属性更新wrapper函数的相应属性。默认值是functools.WRAPPER_UPDATES
      
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

,具体分析见 functools模块
Python标准模块–functools

1. app.route()

首先我们分下Flask类中定义的route实例方法。源码如下:

 def route(self, rule, **options):
    """为给定的URL规则注册一个视图函数的装饰器。它的作用和add_url_rule是相同的,
    不过route是像装饰器来使用。所谓的注册就是将之存在定义的变量(字典or列表)中。
    例如:
        @app.route('/')
        def index():
            return 'Hello World'
    
    :param rule: 字符串类型的URL规则
    :param endpoint: 端点(flask中端点的默认值是视图函数的函数名,如果没有认为设置的话)。
    					要为改URL规则注册的端点。
    :param options: options是一个字典,它会传递给~Werkzeug.routing.Rule这个类的实例对象。
        		methods: methods限定了该rule(规则)所能使用的方法,它是一个list,
        		例如'GET','POST','PUT'等。默认rule所能使用的方法只能是'GET'
    """
    def decorator(f):
        endpoint = options.pop('endpoint', None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator

从上面的源码能看出route()就是一个装饰器,在调用被它装饰的函数之前会调用self.add_url_rule(),那么我们接下来再看add_url_rule()这个方法。源码如下:

@setupmethod
def add_url_rule(self, rule, endpoint=None, view_func=None,
                 provide_automatic_options=None, **options):
    """连接URL规则。像route()装饰器一样工作。如果提供了view_func参数,
    那么此方法会将该视图函数注册为endpoint参数这个端点。
    一个简单的例子是::
        @app.route('/')
        def index():
            pass
    与下边这个等价::
        def index():
            pass
        app.add_url_rule('/', 'index', index)
    如果view_func参数没有提供,那么需要将视图函数和端点关联起来,像下面这样:    
    app.view_functions['index'] = index
    
    :param rule: 字符串类型的URL规则
    :param endpoint: 端点(flask中端点的默认值是视图函数的函数名,如果没有认为设置的话)。
    					要为改URL规则注册的端点。
    :param view_func: 当一个服务器请求了该endpoint是需要调用的函数。也就是这个endpoint对应的视图函数。
    
    :param provide_automatic_options: 控制是否要自动添加'OPTIONS'请求方法。
    			也可以在添加URL规则之前设置view_func.provide_automatic_options = False来控制。
    :param options: options是一个字典,它会传递给~Werkzeug.routing.Rule这个类的实例对象。
        		methods: methods限定了该rule(规则)所能使用的方法,它是一个list,
        		例如'GET','POST','PUT'等。默认rule所能使用的方法只能是'GET'
    """
    if endpoint is None:
    	# _endpoin_from_view_func是flask.helper的一个函数,作用是为函数返回默认的端点(view_func.__name__)
        endpoint = _endpoint_from_view_func(view_func) # 值为view_func.__name__
    options['endpoint'] = endpoint
    methods = options.pop('methods', None)

	# 如果methods没有给定,使用view_func种的methods,如果都不存在则使用默认值('GET',)元组。
    if methods is None:
        methods = getattr(view_func, 'methods', None) or ('GET',)
    if isinstance(methods, string_types):
        raise TypeError('Allowed methods have to be iterables of strings, '
                        'for example: @app.route(..., methods=["POST"])')
    methods = set(item.upper() for item in methods) # 都转换成大写,并去重

    # 获得required_methods
    required_methods = set(getattr(view_func, 'required_methods', ()))

    # 从Flask 0.8开始,view_func对象可以禁用并强制启用automatic options处理。
    if provide_automatic_options is None:
        provide_automatic_options = getattr(view_func,
            'provide_automatic_options', None)

    if provide_automatic_options is None:
        if 'OPTIONS' not in methods:
            provide_automatic_options = True
            required_methods.add('OPTIONS')
        else:
            provide_automatic_options = False

    # 添加required mehtods到methods中
    methods |= required_methods # 集合的并集操作
    
	# url_rule_class就是werkzeug.routing模块中的Rule类
    rule = self.url_rule_class(rule, methods=methods, **options) # 创建Rule的一个实例
    rule.provide_automatic_options = provide_automatic_options

	# url_map是werkzeug.routing模块中Map()的一个实例,作用主要存储URL rule。
    self.url_map.add(rule)
    # 一个endpoint只能对应一个视图函数。
    if view_func is not None:
        old_func = self.view_functions.get(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)
        self.view_functions[endpoint] = view_func

接下来,让我们渠道Werkzeug.routing来交接Rule()和Map()。

2. Rule和Map

Rule继承自RuleFactory,所以让我们先看看RuleFactory。

class RuleFactory(object):
    """只要你有较为复杂的URL设置,最好使用规则工厂(rule factories)来避免重复性任务。
    其中一些是内置的,其他的可以通过继承`RuleFactory`并覆盖`get_rules`来添加。
    """
    def get_rules(self, map):
        """在RuleFactory的子类中需要覆盖这个方法,并且返回值是rule的可迭代对象。
        """
        raise NotImplementedError()

implements_to_string是_compat模块中的一个方法,作用是修改了Rule的几个属性值。细节有需要的同学自己去看吧。

@implements_to_string
class Rule(RuleFactory):
    """Rule代表的是一种URL模式(URL pattern)。需要注意的是除了参数'string'其他都是关键字参数。
    """
     pass
    
class Map(object):
    """Map类存储着所有URL rule和一些配置参数。为了防止影响到所有的rule,有些配置仅仅存在Map实例中。
    除了`rules`参数,其他参数必须都是关键字参数。
    """

这是werkzeu官网中的例子:

from werkzeug.routing import Map, Rule, NotFound, RequestRedirect

url_map = Map([
    Rule('/', endpoint='blog/index'),
    Rule('/<int:year>/', endpoint='blog/archive'),
    Rule('/<int:year>/<int:month>/', endpoint='blog/archive'),
    Rule('/<int:year>/<int:month>/<int:day>/', endpoint='blog/archive'),
    Rule('/<int:year>/<int:month>/<int:day>/<slug>',
         endpoint='blog/show_post'),
    Rule('/about', endpoint='blog/about_me'),
    Rule('/feeds/', endpoint='blog/feeds'),
    Rule('/feeds/<feed_name>.rss', endpoint='blog/show_feed')
])

def application(environ, start_response):
    urls = url_map.bind_to_environ(environ)
    try:
        endpoint, args = urls.match()
    except HTTPException, e:
        return e(environ, start_response)
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Rule points to %r with arguments %r' % (endpoint, args)]

首先,我们创建一个Map的实例,存储了许多的URL规则,这些URL rule在一个list中。
每一个rule都是一个代表URL规则的字符串和一个代表视图函数的端点的实例化。多个规则可以具有相同的端点,但应该具有不同的参数以允许URL构造。
在WSGI应用程序内部,我们将url_map绑定到当前请求,该请求将返回一个新的MapAdapter。然后,可以使用此url_map适配器来匹配或构建当前请求的域名。

也就是说在定义URL规则的时候,需要注意:(来自 Flask中url详解)

在定义url的时候,一定要记得在最后加一个斜杠, 如果不加斜杠,那么在浏览器中访问这个url的时候,如果加了斜杠,那么就访问不到。这样用户体验不好。
搜索引擎会将不加斜杠和加斜杠的视为两个不同的url,其实是同一个url。

通过route装饰器(或add_url_rule),就将此URL path和视图函数创建了一个Rule实例,并将此实例加入了url_map(是Map()的一个实例,作用就是存储URL rules)

3. 总结

werkzeug中的Rule类将URL和endpoint关联起来,Map类也就是存储Rule类的实例的;
flask中的route是将endpoint和视图函数关联起来的。
所以最终通过URL能够找到对应的视图函数,这就是路由的实现。

4. 参考文献

[1] 用尽洪荒之力学习Flask源码
[2] Flask中url详解

 类似资料: