zoukankan      html  css  js  c++  java
  • python框架之Flask(2)-路由和视图&Session

    路由和视图

    这一波主要是通过看源码加深对 Flask 中路由和视图的了解,可以先回顾一下装饰器的知识:【装饰器函数与进阶

    路由设置的两种方式

    # 示例代码
    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app.route('/index')
    def index():
        return 'index'
    
    
    if __name__ == '__main__':
        app.run()

    直接看上面代码,在 index 方法上通过装饰器 @app.route('/index') 就建立路由 '/index' 和方法 index 的对应关系。

    查看 app.route 的源码:

    1 def route(self, rule, **options):
    2     def decorator(f):
    3         endpoint = options.pop('endpoint', None)
    4         self.add_url_rule(rule, endpoint, f, **options)
    5         return f
    6 
    7     return decorator
    flask.app.Flask.route

    在上述示例中, rule 就是我们自定义的路由参数 '/index' ; endpoint 就是终结点参数(用来反向生成 url),这里我们没传;而 f 实际上就是该装饰器所装饰的函数,在这里也就是 index 函数。其实到这里就可以断定,该装饰器实际上就是通过第 4 行的 add_url_rule 函数来建立路由和视图的对应关系。我们可以测试:

    from flask import Flask
    
    app = Flask(__name__)
    
    
    def index():
        return 'index'
    
    
    app.add_url_rule('/index', view_func=index)
    
    if __name__ == '__main__':
        app.run()

    在这里我去掉了 index 函数的装饰器,而直接通过 app.add_url_rule 函数建立路由 '/index' 和 index 函数的对应关系,正常访问。

    endpoint

    接着看 app.add_url_rule 函数做了什么:

     1 @setupmethod
     2 def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options):
     3     if endpoint is None:
     4         endpoint = _endpoint_from_view_func(view_func)
     5     options['endpoint'] = endpoint
     6     methods = options.pop('methods', None)
     7 
     8     if methods is None:
     9         methods = getattr(view_func, 'methods', None) or ('GET',)
    10     if isinstance(methods, string_types):
    11         raise TypeError('Allowed methods have to be iterables of strings, '
    12                         'for example: @app.route(..., methods=["POST"])')
    13     methods = set(item.upper() for item in methods)
    14 
    15     required_methods = set(getattr(view_func, 'required_methods', ()))
    16 
    17     if provide_automatic_options is None:
    18         provide_automatic_options = getattr(view_func,
    19                                             'provide_automatic_options', None)
    20 
    21     if provide_automatic_options is None:
    22         if 'OPTIONS' not in methods:
    23             provide_automatic_options = True
    24             required_methods.add('OPTIONS')
    25         else:
    26             provide_automatic_options = False
    27 
    28     methods |= required_methods
    29 
    30     rule = self.url_rule_class(rule, methods=methods, **options)
    31     rule.provide_automatic_options = provide_automatic_options
    32 
    33     self.url_map.add(rule)
    34     if view_func is not None:
    35         old_func = self.view_functions.get(endpoint)
    36         if old_func is not None and old_func != view_func:
    37             raise AssertionError('View function mapping is overwriting an '
    38                                  'existing endpoint function: %s' % endpoint)
    39         self.view_functions[endpoint] = view_func
    flask.app.Flask.add_url_rule

    看 3、4、5 行,在示例中我们并没有传入 endpoint 参数,所以 endpoint 在第 3 行肯定是 None。接着执行第 4 行,查看 _endpoint_from_view_func 方法:

    1 def _endpoint_from_view_func(view_func):
    2     assert view_func is not None, 'expected view func if endpoint ' 
    3                                   'is not provided.'
    4     return view_func.__name__
    flask.helpers._endpoint_from_view_func

    看第 4 行的返回值是视图函数的函数名称,所以当不传 endpoint 参数时, endpoint 的值就是视图函数的函数名称

    继续看 flask.app.Flask.add_url_rule 函数的 34-39 行, 39 行做的就是每次装饰器执行时,就会把装饰器当前装饰的函数当做值, endpoint 当做 key ,放入 view_functions 这个字典中。而从 35-38 行可以看到,如果一个新的视图函数的 endpoint 已经存在 view_functions 中,但这个函数又与 endpoint 之前对应的视图函数不是同一个函数,就会产生 37 行错误。所以我们要保证每个视图函数对应的 endpoint 不重复

    app.route的参数

    除了我们已经熟悉的 rule 和 view_func ,它还可传如下参数:

    • defaults

      默认值,当URL中无参数,但函数需要参数时,可以使用 defaults={'k':'v'} 为函数提供参数。

    • endpoint

      名称,用于反向生成 URL,即:  url_for('endpoint') 。

    • methods

      允许的请求方式,如: methods=["GET","POST"] 。

    • strict_slashes

      对 URL 最后的 '/' 符号是否严格要求。

      @app.route('/index',strict_slashes=False) # 访问 http://www.xx.com/index/ 或 http://www.xx.com/index均可                                           
      @app.route('/index',strict_slashes=True) # 仅访问 http://www.xx.com/index 
    • redirect

      重定向到指定地址。

      @app.route('/index/<int:nid>', redirect_to='/home/<nid>')
      def index():
          return 'index'
    • subdomain

      子域名访问。

      from flask import Flask
      
      app = Flask(import_name=__name__)
      app.config['SERVER_NAME'] = 'zze.com:5000'
      
      
      @app.route("/", subdomain="admin")  # admin.zze.com:5000
      def admin_index():
          return "admin"
      
      
      @app.route("/", subdomain="guest")  # guest.zze.com:5000
      def guest_index():
          return "guest"
      
      
      @app.route("/dynamic", subdomain="<username>")  # http://test.zze.com:5000/dynamic
      def dynamic_index(username):
          return username
      
      
      if __name__ == '__main__':
          app.run()

    CBV

    from flask import Flask, views
    
    app = Flask(__name__)
    
    
    class TestView(views.MethodView):
        methods = ['GET']  # 只支持 GET 请求
        decorators = []  # 批量加上装饰器
    
        def get(self, *args, **kwargs):
            return 'GET'
    
        def post(self, *args, **kwargs):
            return 'POST'
    
    
    app.add_url_rule('/test', None, TestView.as_view('test'))  # as_view 的参数就是 endpoint
    
    if __name__ == '__main__':
        app.run()

    它的实现其实和 Django 中的 CBV 实现很相似,源码就不细说了。

    正则匹配URL

    from flask import Flask, views, url_for
    from werkzeug.routing import BaseConverter
    
    app = Flask(import_name=__name__)
    
    
    # 自定制类
    class RegexConverter(BaseConverter):
        """
        自定义URL匹配正则表达式
        """
    
        def __init__(self, map, regex):
            super(RegexConverter, self).__init__(map)
            self.regex = regex
    
        def to_python(self, value):
            """
            路由匹配时,匹配成功后传递给视图函数中参数的值
            :param value:
            :return:
            """
            return int(value)
    
        def to_url(self, value):
            """
            使用url_for反向生成URL时,传递的参数经过该方法处理,返回的值用于生成URL中的参数
            :param value:
            :return:
            """
            val = super(RegexConverter, self).to_url(value)
            return val
    
    
    # 注册到 flask 的转换器中
    app.url_map.converters['regex'] = RegexConverter
    
    
    # 使用
    @app.route('/index/<regex("d+"):nid>')
    def index(nid):
        print(url_for('index', nid='888'))
        return 'Index'
    
    
    if __name__ == '__main__':
        app.run()

    Session

    源码

    首先我们要知道 Flask 初执行是会经过 flask.app.Flask.__call__ 方法的,可以参考【Flask 的入口】。

    def __call__(self, environ, start_response):
        # environ :请求相关所有数据
        # start_response :用于设置响应相关数据
        return self.wsgi_app(environ, start_response)

    再查看 wsgi_app 方法:

     1 def wsgi_app(self, environ, start_response):
     2     '''
     3     获取environ并对其进行封装
     4     从environ中获取名为session的cookie,解密并反序列化
     5     放入请求上下文
     6     '''
     7     ctx = self.request_context(environ)
     8     error = None
     9     try:
    10         try:
    11             ctx.push()
    12             '''
    13             执行视图函数
    14             '''
    15             response = self.full_dispatch_request()
    16         except Exception as e:
    17             error = e
    18             response = self.handle_exception(e)
    19         except:
    20             error = sys.exc_info()[1]
    21             raise
    22         return response(environ, start_response)
    23     finally:
    24         if self.should_ignore_error(error):
    25             error = None
    26         '''
    27         获取session,解密并序列化,写入cookie
    28         清空请求上下文
    29         '''
    30         ctx.auto_pop(error)
    flask.app.Flask.wsgi_app

    environ 是请求相关信息,第 7 行将 environ 传入 request_context 方法,看一下:

    1 def request_context(self, environ):
    2     return RequestContext(self, environ)
    flask.app.Flask.request_context

    可以看到它的返回值就是以 environ 为构造参数传入 RequestContext 类中的一个实例,看一下它初始化时做了什么:

     1 def __init__(self, app, environ, request=None):
     2     self.app = app
     3     if request is None:
     4         request = app.request_class(environ)
     5     self.request = request
     6     self.url_adapter = app.create_url_adapter(self.request)
     7     self.flashes = None
     8     self.session = None
     9 
    10     self._implicit_app_ctx_stack = []
    11 
    12     self.preserved = False
    13 
    14     self._preserved_exc = None
    15 
    16     self._after_request_functions = []
    17 
    18     self.match_request()
    flask.ctx.RequestContext.__init__

    看 3-8 行, environ 传入 request_class 方法中返回一个 request 实例,赋值给 self ,并在第 8 行给 self 新增一个 session 属性并赋值为 None 。而最终这个 self 在 flask.app.Flask.wsgi_app 的第 7 行赋值给 ctx 。总结一下就是在执行完 flask.app.Flask.wsgi_app 的第 7 行后, ctx 被赋值为 RequestContext 的一个实例,且这个实例中存在了将 environ 再次封装的属性 request 和一个为 None 的属性 session 

    接着看到 flask.app.Flask.wsgi_app 中的第 11 行,查看 push 方法:

     1 def push(self):
     2     top = _request_ctx_stack.top
     3     if top is not None and top.preserved:
     4         top.pop(top._preserved_exc)
     5 
     6     app_ctx = _app_ctx_stack.top
     7     if app_ctx is None or app_ctx.app != self.app:
     8         app_ctx = self.app.app_context()
     9         app_ctx.push()
    10         self._implicit_app_ctx_stack.append(app_ctx)
    11     else:
    12         self._implicit_app_ctx_stack.append(None)
    13 
    14     if hasattr(sys, 'exc_clear'):
    15         sys.exc_clear()
    16 
    17     _request_ctx_stack.push(self)
    18 
    19     if self.session is None:
    20         session_interface = self.app.session_interface
    21         self.session = session_interface.open_session(
    22             self.app, self.request
    23         )
    24 
    25         if self.session is None:
    26             self.session = session_interface.make_null_session(self.app)
    flask.ctx.RequestContext.push

    直接看 19-26 行。如果 session 为空,就将传入 app 和 request 参数执行 session_interface 的 open_session 方法的返回值赋给 session ,而此时这个 session_interface 默认就是 flask.sessions.SecureCookieSessionInterface 类的实例,查看其 open_session 方法:

     1 def open_session(self, app, request):
     2     s = self.get_signing_serializer(app)
     3     if s is None:
     4         return None
     5     val = request.cookies.get(app.session_cookie_name)
     6     if not val:
     7         return self.session_class()
     8     max_age = total_seconds(app.permanent_session_lifetime)
     9     try:
    10         data = s.loads(val, max_age=max_age)
    11         return self.session_class(data)
    12     except BadSignature:
    13         return self.session_class()
    flask.sessions.SecureCookieSessionInterface.open_session

    看第 5 行是从 cookie 中取一个键为 app.session_cookie_name 的值,而这个键的默认值就是 'session' ,可在配置文件中配置(点击查看配置文件默认配置参数)。紧接着就将这个值反序列化传入 session_class 并返回 session_class 的实例,而 session_class 对应的是类 flask.sessions.SecureCookieSession 。所以在上面 flask.ctx.RequestContext.push 方法中 21 行 self.session 的值就是 flask.sessions.SecureCookieSession 的实例。也就是在上面 flask.app.Flask.wsgi_app 的 11 行执行之后, ctx 的 session 就有值了。

    接着看 flask.app.Flask.wsgi_app 的第 15 行,这行就是通过 full_dispatch_request 方法来完成执行视图函数和部分请求的收尾操作:

     1 def full_dispatch_request(self):
     2     self.try_trigger_before_first_request_functions()
     3     try:
     4         request_started.send(self)
     5         rv = self.preprocess_request()
     6         if rv is None:
     7             rv = self.dispatch_request()
     8     except Exception as e:
     9         rv = self.handle_user_exception(e)
    10     return self.finalize_request(rv)
    flask.app.Flask.full_dispatch_request

    看第 10 行,在完成了上面视图函数相关操作后,通过 finalize_request 方法完成请求收尾操作:

     1 def finalize_request(self, rv, from_error_handler=False):
     2     response = self.make_response(rv)
     3     try:
     4         response = self.process_response(response)
     5         request_finished.send(self, response=response)
     6     except Exception:
     7         if not from_error_handler:
     8             raise
     9         self.logger.exception('Request finalizing failed with an '
    10                               'error while handling an error')
    11     return response
    flask.app.Flask.finalize_request

    再看到第 4 行的 process_response 方法,这个方法如其名,就是用来处理响应相关信息:

     1 def process_response(self, response):
     2     ctx = _request_ctx_stack.top
     3     bp = ctx.request.blueprint
     4     funcs = ctx._after_request_functions
     5     if bp is not None and bp in self.after_request_funcs:
     6         funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
     7     if None in self.after_request_funcs:
     8         funcs = chain(funcs, reversed(self.after_request_funcs[None]))
     9     for handler in funcs:
    10         response = handler(response)
    11     if not self.session_interface.is_null_session(ctx.session):
    12         self.session_interface.save_session(self, ctx.session, response)
    13     return response
    flask.app.process_response

    直接看 11、12 行,当 session 不为空时,调用 session_interface.save_session 方法,而 session_interface 就是上面执行 open_session 方法的 flask.sessions.SecureCookieSessionInterface 类实例。

     1 def save_session(self, app, session, response):
     2     domain = self.get_cookie_domain(app)
     3     path = self.get_cookie_path(app)
     4 
     5     if not session:
     6         if session.modified:
     7             response.delete_cookie(
     8                 app.session_cookie_name,
     9                 domain=domain,
    10                 path=path
    11             )
    12 
    13         return
    14 
    15     if session.accessed:
    16         response.vary.add('Cookie')
    17 
    18     if not self.should_set_cookie(app, session):
    19         return
    20 
    21     httponly = self.get_cookie_httponly(app)
    22     secure = self.get_cookie_secure(app)
    23     samesite = self.get_cookie_samesite(app)
    24     expires = self.get_expiration_time(app, session)
    25     val = self.get_signing_serializer(app).dumps(dict(session))
    26     response.set_cookie(
    27         app.session_cookie_name,
    28         val,
    29         expires=expires,
    30         httponly=httponly,
    31         domain=domain,
    32         path=path,
    33         secure=secure,
    34         samesite=samesite
    35     )
    flask.sessions.SecureCookieSessionInterface.save_session

    看 25-35 行,会发现最后又将 session 序列化,再次写入 cookie 。

    得出结论:当请求刚到来时,flask 读取 cookie 中 session 对应的值,并将该值解密并反序列化成字典,放入内存以便视图函数使用;当请求结束时,flask 会读取内存中字典的值,进行序列化和加密,再写入到 cookie 中。

    第三方session

    • 使用

       1 from flask import Flask, request, session, redirect
       2 from flask.sessions import SecureCookieSessionInterface
       3 from flask_session import Session
       4 from redis import Redis
       5 
       6 app = Flask(__name__)
       7 app.debug = True
       8 
       9 
      10 app.config['SESSION_REDIS'] = Redis(host='127.0.0.1', port='6379', password='1234')
      11 # 设置 session 类型
      12 app.config['SESSION_TYPE'] = 'redis'
      13 # 设置 根据 session 类型设置 app.session_interface
      14 Session(app)
      15 
      16 
      17 @app.route('/login')
      18 def login():
      19     session['username'] = 'zze'
      20     return 'success'
      21 
      22 
      23 app.run()
    • 源码

      通过上面的源码部分已经知道了,flask 中的 session 存取就是通过 app.session_interface 来完成的,默认 app.session_interface = SecureCookieSessionInterface() ,而我们只要修改这一部分,让其存取是通过 redis 就 ok 了。查看 14 行 Session 类:

       1 class Session(object):
       2     def __init__(self, app=None):
       3         self.app = app
       4         if app is not None:
       5             self.init_app(app)
       6 
       7     def init_app(self, app):
       8         app.session_interface = self._get_interface(app)
       9 
      10     def _get_interface(self, app):
      11         config = app.config.copy()
      12         config.setdefault('SESSION_TYPE', 'null')
      13         config.setdefault('SESSION_PERMANENT', True)
      14         config.setdefault('SESSION_USE_SIGNER', False)
      15         config.setdefault('SESSION_KEY_PREFIX', 'session:')
      16         config.setdefault('SESSION_REDIS', None)
      17         config.setdefault('SESSION_MEMCACHED', None)
      18         config.setdefault('SESSION_FILE_DIR',
      19                           os.path.join(os.getcwd(), 'flask_session'))
      20         config.setdefault('SESSION_FILE_THRESHOLD', 500)
      21         config.setdefault('SESSION_FILE_MODE', 384)
      22         config.setdefault('SESSION_MONGODB', None)
      23         config.setdefault('SESSION_MONGODB_DB', 'flask_session')
      24         config.setdefault('SESSION_MONGODB_COLLECT', 'sessions')
      25         config.setdefault('SESSION_SQLALCHEMY', None)
      26         config.setdefault('SESSION_SQLALCHEMY_TABLE', 'sessions')
      27 
      28         if config['SESSION_TYPE'] == 'redis':
      29             session_interface = RedisSessionInterface(
      30                 config['SESSION_REDIS'], config['SESSION_KEY_PREFIX'],
      31                 config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT'])
      32         elif config['SESSION_TYPE'] == 'memcached':
      33             session_interface = MemcachedSessionInterface(
      34                 config['SESSION_MEMCACHED'], config['SESSION_KEY_PREFIX'],
      35                 config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT'])
      36         elif config['SESSION_TYPE'] == 'filesystem':
      37             session_interface = FileSystemSessionInterface(
      38                 config['SESSION_FILE_DIR'], config['SESSION_FILE_THRESHOLD'],
      39                 config['SESSION_FILE_MODE'], config['SESSION_KEY_PREFIX'],
      40                 config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT'])
      41         elif config['SESSION_TYPE'] == 'mongodb':
      42             session_interface = MongoDBSessionInterface(
      43                 config['SESSION_MONGODB'], config['SESSION_MONGODB_DB'],
      44                 config['SESSION_MONGODB_COLLECT'],
      45                 config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'],
      46                 config['SESSION_PERMANENT'])
      47         elif config['SESSION_TYPE'] == 'sqlalchemy':
      48             session_interface = SqlAlchemySessionInterface(
      49                 app, config['SESSION_SQLALCHEMY'],
      50                 config['SESSION_SQLALCHEMY_TABLE'],
      51                 config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'],
      52                 config['SESSION_PERMANENT'])
      53         else:
      54             session_interface = NullSessionInterface()
      55 
      56         return session_interface
      flask_session.Session

      执行到第 8 行,可以看到它就是给 app.session_interface 赋值为 self._get_interface(app) ,而这个方法的返回值是根据在上面使用中第 12 行配置的 'SESSION_TYPE' 字段决定的,这里设置的是 'redis' ,所以 self._get_interface(app) 的返回值就为第 82 行的 RedisSessionInterface 实例。

  • 相关阅读:
    为什么大家都说 SELECT * 效率低?
    一个 Java 方法,最多能定义多少参数?
    牛逼哄哄的布隆过滤器,到底有什么用?
    10w+ Excel 数据导入,怎么优化?
    为什么 wait 方法要在 synchronized 中调用?
    使用Redis存储聊天数据的一种方案(使用lua解决原子性问题)
    Linux下安装redis
    Django Rest Framework组件:解析器JSONParser、FormParser、MultiPartParser、FileUploadParser
    API测试之Postman使用全指南(转载)
    Django Rest Framework组件:序列化与反序列化模块Serializer
  • 原文地址:https://www.cnblogs.com/zze46/p/10113826.html
Copyright © 2011-2022 走看看