awesome Flask

本文总阅读量
  56 mins to read  

BBS部署于:http://bbs.iv4n.xyz,,项目地址

接下来一点一点总结踩过的坑们:

  • flask_xxx,新版的Flask扩展把flask.ext.xxx改为了前面那个,我觉得算退步吧,一个ext包模块化结构不是清晰很多吗

  • flask_loginuser必须要有id属性,因为我用了MongoDB所以开始没赋id,之后看了文档解决

    class UserMixin(object):
      
        if not PY2:  # pragma: no cover
            # Python 3 implicitly set __hash__ to None if we override __eq__
            # We set it back to its default implementation
            __hash__ = object.__hash__
      
        @property
        def is_active(self):
            return True
      
        @property
        def is_authenticated(self):
            return True
      
        @property
        def is_anonymous(self):
            return False
      
        def get_id(self):
            try:
                return text_type(self.id)
            except AttributeError:
                raise NotImplementedError('No `id` attribute - override `get_id`')
    
  • Flask的蓝图,这个东西通俗点说就是让项目能更具模块化,比如说同类视图的实现放在同一个文件里,然后不同功能创建不同文件,但假如没有蓝图很难实现,因为涉及相互导入的问题,即a包导入b包,b包中又导入b包,而有了蓝图的话可以先实现功能,然后在项目的__init__.py中为app类注册实现功能蓝图

  • 相对包导入,这个写多自然就懂了。简单点说,Python3中使用from .. import xxx/from ..a import xxx这种叫做相对包导入,假如写函数库的话这样会为调用者省掉很多麻烦,但是!:一不小心报错。因为相对导入只有在包内可实现,而包必须在顶层文件夹内包含__init__.py,即使它为空。举例:

    /test
    |___ a.py(内含app实例)
    |___ /routes
    		|___ b.py(需导入app实例)
    

    我在b.py里写from ..a import app,报错ValueError: attempted relative import beyond top-level package因为test本身不是一个包,所以b.py无法相对导入上层文件夹里的变量,解决办法:在/test下创建__init__,然后将/test当做包,在/test同级目录下创建文件调用b.py。在Flask开发时,我在项目文件夹中的__init__.py中实例化app/mongo/bootstrap,然后在项目文件夹同级创建启动文件,启动文件中再为app注册蓝图。假如不在外层启动的话,__init__.py需要导入数据库蓝图注册,而数据库蓝图文件中又需要导入__init__.py来实例化数据库对象,这就造成了相互导入,不报错就怪了

    另外__init__.py中的变量是会被提到比包内文件高一层的位置上,即直接是包级文件,举个例子:

    /a
    |___ b.py(内含class t)
    |___ __init__.py
    

    这里假如我从外部导入类t的话需要from a.b import t,但假如我在__init__.py中写from .b import t,我再从外部导入就只需要from a import t,即把t提升到了包级

  • flask_login中的user.loader回调,文档中告诉你要实现函数:

    @login_manager.user_loader
    def load_user(userid):
        return get_user_obj(userid) if xxx else None
    

    即通过这个函数返回id对应的用户对象,想想flask_login的内部实现就可以理解,它在内部维护了一个登录用户的实例栈,通过每个用户唯一的id来获取用户实例,可以阅读源码(注释写的非常清楚了):

    def user_loader(self, callback):
            '''
            This sets the callback for reloading a user from the session. The
            function you set should take a user ID (a ``unicode``) and return a
            user object, or ``None`` if the user does not exist.
      
            :param callback: The callback for retrieving a user object.
            :type callback: callable
            '''
            self.user_callback = callback
            return callback
      
    def reload_user(self, user=None):
            '''
            This set the ctx.user with the user object loaded by your customized
            user_loader callback function, which should retrieved the user object
            with the user_id got from session.
      
            Syntax example:
            from flask_login import LoginManager
            @login_manager.user_loader
            def any_valid_func_name(user_id):
                # get your user object using the given user_id,
                # if you use SQLAlchemy, for example:
                user_obj = User.query.get(int(user_id))
                return user_obj
      
            Reason to let YOU define this self.user_callback:
                Because we won't know how/where you will load you user object.
            '''
            ctx = _request_ctx_stack.top
      
            if user is None:
                user_id = session.get('user_id')
                if user_id is None:
                    ctx.user = self.anonymous_user()
                else:
                    if self.user_callback is None:
                        raise Exception(
                            "No user_loader has been installed for this "
                            "LoginManager. Refer to"
                            "https://flask-login.readthedocs.io/"
                            "en/latest/#how-it-works for more info.")
                    user = self.user_callback(user_id)
                    if user is None:
                        ctx.user = self.anonymous_user()
                    else:
                        ctx.user = user
            else:
                ctx.user = user
    

    这里因为我用的MongoDB,所以网上没有现成实现,几乎都是SqlArchemy的,这里我给用户类添加了一个类方法

    class BaseUser(UserMixin):
          
        __slots__ = ['id', 'uname', 'passwd', 'passwd_hash', 'email', 'role']
          
        def __init__(self, _id, uname=None, passwd=None, email=None, role="basic"):
            self.id=_id
            self.uname = uname
            self.passwd = passwd
            self.email = email
            self.role = role
            self.passwd_hash = hashlib.sha256(passwd.encode('utf-8')+(hashlib.md5(salt.encode("utf-8")).hexdigest()).encode("utf-8")).hexdigest()
          
    ## Other func
      
        @classmethod
        def query(cls, user_id):
            result = mongo.db.users.find_one({"_id": ObjectId(user_id)})
            return cls(result["_id"], result["uname"], result["passwd"], result["email"], result["role"])
    
  • MongoDB的数据库权限,MongoDB的未授权访问漏洞是因为未开启数据库auth,正确做法是先关闭auth然后添加数据库用户,添加root用户需到admin库,别的数据库管理员需到指定库,也就是说添加那个库权限就到哪个库执行createUser()(MongoDB 3.0以前是addUser),然后开启auth在开放到公网,不过一般问题也不大,别监听0.0.0.0就行了。此外,MongoDB的权限也很奇怪,dbAdmin居然没有读写数据库权限(一直说我无权限执行函数搞得我很迷惑),只有执行管理函数的权限,而读写权限需要readWrite,如下:

    /*
    * Read:允许用户读取指定数据库
    * readWrite:允许用户读写指定数据库
    * dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profile
    * userAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户
    * clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限。
    * readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限
    * readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限
    * userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
    * dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限。
    * root:只在admin数据库中可用。超级账号,超级权限
    */
      
    > use flask
    > db.createUser({
          "user": "flask",
          "pwd": "flask",
          "roles": [
              "role": "readWrite",
              "db": "flask"
          ]
      })
    
  • 蓝图中的ulr_for函数,这个函数的前缀是需要实例化蓝图时的名字的,如

    main = Blueprint("a", __name__)
    url_for("a.xxx")
    
  • 头像,头像我没有保存到项目文件里然后open()啥的,我直接把图片二进制base64编码然后存进数据库了,或者其实不编码直接存也可以,因为MongoDB本身就是Bson格式存的数据,然后在后台创建了一个读取头像的API

  • sort分页,我这样写的get_posts函数,按页码select数据:

    def get_posts_api(limit=0, skip=0, **kwargs):
        for i in kwargs.keys():
            if type(i) != str or type(i) != int:
                continue
            kwargs[i], _ = flask_real_escape(kwargs[i])  
        _posts = mongo.db.posts.find(kwargs).sort([("date", -1)]).skip(skip).limit(limit)
        posts = list(_posts)
        for i in posts:
            i["content"] = mistune.markdown(i["content"], escape=True, hard_wrap=True)
        return posts
    

    其中的sort函数查阅文档可知需要传进去一个元组列表,如sort([(a, 1), (b, -1)])

  • redirect函数,我服务器前加了一层Nginx反代,因为我总觉得Gunicorn是个玩具,丢在公网上实在不放心,就只让它监听本地让Nginx去请求它。Redirect会返回重定向的URL,然后set location响应头,Nginx配置需要加上proxy_set_header,不然redirect会直接返回127.0.0.1:2333/xxx到用户浏览器

    server {
            listen 80;
            server_name 39.105.187.104;
            location / {
                    proxy_pass http://127.0.0.1:2333;
                    proxy_redirect off;
                    proxy_set_header Host $host:$server_port;
            }
    }
    
  • Gunicorn的部署,直接gunicorn wsgi:app是不行的,提示找不到app对象,因为我的启动函数不在项目包里,而直接从包启动又没有注册蓝图。没办法看文档另一种方法创建工厂函数,改写gunicorn 'wsgi:create_app()',ok

  • 注册的验证码我用了一个随机四个数的运算,当然其实没有什么卵用,恶意爬虫直接爬下来eval就完事了,我可能就是好玩吧。最初我是在routes/auth.py中定义了全局变量的随机生成,本地测试一切正常(缓存的缘故),部署后就崩了,原因是第一次访问注册页生成一个验证码,然后post数据的时候算作第二次访问,这时候验证码已经刷新了,而用户提交的依旧是第一次访问生成在html里的验证码。解决办法将验证码保存到session全局变量中即可


Flask && Werkzeug源码阅读

Flask

其实Flask框架整体最亮眼的是它的四个全局变量概念(session/current_app/request/g)以及上下文的概念,全局变量的实现是在WerkzeugLocal模块,所以会着重学习它的实现

上下文的实现:

class _AppCtxGlobals(object):
    """A plain object. Used as a namespace for storing data during an
    application context.

    Creating an app context automatically creates this object, which is
    made available as the :data:`g` proxy.

    .. describe:: 'key' in g

        Check whether an attribute is present.

        .. versionadded:: 0.10

    .. describe:: iter(g)

        Return an iterator over the attribute names.

        .. versionadded:: 0.10
    """

    def get(self, name, default=None):
        """Get an attribute by name, or a default value. Like
        :meth:`dict.get`.

        :param name: Name of attribute to get.
        :param default: Value to return if the attribute is not present.

        .. versionadded:: 0.10
        """
        return self.__dict__.get(name, default)

    def pop(self, name, default=_sentinel):
        """Get and remove an attribute by name. Like :meth:`dict.pop`.

        :param name: Name of attribute to pop.
        :param default: Value to return if the attribute is not present,
            instead of raise a ``KeyError``.

        .. versionadded:: 0.11
        """
        if default is _sentinel:
            return self.__dict__.pop(name)
        else:
            return self.__dict__.pop(name, default)

    def setdefault(self, name, default=None):
        """Get the value of an attribute if it is present, otherwise
        set and return a default value. Like :meth:`dict.setdefault`.

        :param name: Name of attribute to get.
        :param: default: Value to set and return if the attribute is not
            present.

        .. versionadded:: 0.11
        """
        return self.__dict__.setdefault(name, default)

    def __contains__(self, item):
        return item in self.__dict__

    def __iter__(self):
        return iter(self.__dict__)

    def __repr__(self):
        top = _app_ctx_stack.top
        if top is not None:
            return '<flask.g of %r>' % top.app.name
        return object.__repr__(self)
    
class AppContext(object):
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

        # Like request context, app contexts can be pushed multiple times
        # but there a basic "refcount" is enough to track them.
        self._refcnt = 0

    def push(self):
        """Binds the app context to the current context."""
        self._refcnt += 1
        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()
        _app_ctx_stack.push(self)
        appcontext_pushed.send(self.app)

    def pop(self, exc=_sentinel):
        """Pops the app context."""
        try:
            self._refcnt -= 1
            if self._refcnt <= 0:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_appcontext(exc)
        finally:
            rv = _app_ctx_stack.pop()
        assert rv is self, 'Popped wrong app context.  (%r instead of %r)' \
            % (rv, self)
        appcontext_popped.send(self.app)

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)


class RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.

    Do not attempt to use this class directly, instead use
    :meth:`~flask.Flask.test_request_context` and
    :meth:`~flask.Flask.request_context` to create this object.

    When the request context is popped, it will evaluate all the
    functions registered on the application for teardown execution
    (:meth:`~flask.Flask.teardown_request`).

    The request context is automatically popped at the end of the request
    for you.  In debug mode the request context is kept around if
    exceptions happen so that interactive debuggers have a chance to
    introspect the data.  With 0.4 this can also be forced for requests
    that did not fail and outside of ``DEBUG`` mode.  By setting
    ``'flask._preserve_context'`` to ``True`` on the WSGI environment the
    context will not pop itself at the end of the request.  This is used by
    the :meth:`~flask.Flask.test_client` for example to implement the
    deferred cleanup functionality.

    You might find this helpful for unittests where you need the
    information from the context local around for a little longer.  Make
    sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
    that situation, otherwise your unittests will leak memory.
    """

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.flashes = None
        self.session = None

        # Request contexts can be pushed multiple times and interleaved with
        # other request contexts.  Now only if the last level is popped we
        # get rid of them.  Additionally if an application context is missing
        # one is created implicitly so for each level we add this information
        self._implicit_app_ctx_stack = []

        # indicator if the context was preserved.  Next time another context
        # is pushed the preserved context is popped.
        self.preserved = False

        # remembers the exception for pop if there is one in case the context
        # preservation kicks in.
        self._preserved_exc = None

        # Functions that should be executed after the request on the response
        # object.  These will be called before the regular "after_request"
        # functions.
        self._after_request_functions = []

        self.match_request()

    def _get_g(self):
        return _app_ctx_stack.top.g
    def _set_g(self, value):
        _app_ctx_stack.top.g = value
    g = property(_get_g, _set_g)
    del _get_g, _set_g

    def copy(self):
        """Creates a copy of this request context with the same request object.
        This can be used to move a request context to a different greenlet.
        Because the actual request object is the same this cannot be used to
        move a request context to a different thread unless access to the
        request object is locked.

        .. versionadded:: 0.10
        """
        return self.__class__(self.app,
            environ=self.request.environ,
            request=self.request
        )

    def match_request(self):
        """Can be overridden by a subclass to hook into the matching
        of the request.
        """
        try:
            url_rule, self.request.view_args = \
                self.url_adapter.match(return_rule=True)
            self.request.url_rule = url_rule
        except HTTPException as e:
            self.request.routing_exception = e

    def push(self):
        """Binds the request context to the current context."""
        # If an exception occurs in debug mode or if context preservation is
        # activated under exception situations exactly one context stays
        # on the stack.  The rationale is that you want to access that
        # information under debug situations.  However if someone forgets to
        # pop that context again we want to make sure that on the next push
        # it's invalidated, otherwise we run at risk that something leaks
        # memory.  This is usually only a problem in test suite since this
        # functionality is not active in production environments.
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)

        # Before we push the request context we have to ensure that there
        # is an application context.
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()

        _request_ctx_stack.push(self)

        # Open the session at the moment that the request context is available.
        # This allows a custom open_session method to use the request context.
        # Only open a new session if this is the first time the request was
        # pushed, otherwise stream_with_context loses the session.
        if self.session is None:
            session_interface = self.app.session_interface
            self.session = session_interface.open_session(
                self.app, self.request
            )

            if self.session is None:
                self.session = session_interface.make_null_session(self.app)

    def pop(self, exc=_sentinel):
        """Pops the request context and unbinds it by doing that.  This will
        also trigger the execution of functions registered by the
        :meth:`~flask.Flask.teardown_request` decorator.

        .. versionchanged:: 0.9
           Added the `exc` argument.
        """
        app_ctx = self._implicit_app_ctx_stack.pop()

        try:
            clear_request = False
            if not self._implicit_app_ctx_stack:
                self.preserved = False
                self._preserved_exc = None
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_request(exc)

                # If this interpreter supports clearing the exception information
                # we do that now.  This will only go into effect on Python 2.x,
                # on 3.x it disappears automatically at the end of the exception
                # stack.
                if hasattr(sys, 'exc_clear'):
                    sys.exc_clear()

                request_close = getattr(self.request, 'close', None)
                if request_close is not None:
                    request_close()
                clear_request = True
        finally:
            rv = _request_ctx_stack.pop()

            # get rid of circular dependencies at the end of the request
            # so that we don't require the GC to be active.
            if clear_request:
                rv.request.environ['werkzeug.request'] = None

            # Get rid of the app as well if necessary.
            if app_ctx is not None:
                app_ctx.pop(exc)

            assert rv is self, 'Popped wrong request context.  ' \
                '(%r instead of %r)' % (rv, self)

    def auto_pop(self, exc):
        if self.request.environ.get('flask._preserve_context') or \
           (exc is not None and self.app.preserve_context_on_exception):
            self.preserved = True
            self._preserved_exc = exc
        else:
            self.pop(exc)

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        # do not pop the request stack if we are in debug mode and an
        # exception happened.  This will allow the debugger to still
        # access the request object in the interactive shell.  Furthermore
        # the context can be force kept alive for the test client.
        # See flask.testing for how this works.
        self.auto_pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)

    def __repr__(self):
        return '<%s \'%s\' [%s] of %s>' % (
            self.__class__.__name__,
            self.request.url,
            self.request.method,
            self.app.name,
        )

因为它的底层实现是依赖Local类型,所以先看werkzeug中的实现


Werkzeug

Werkzeug是一个wsgi工具集,实现了web的常用功能,如request/response。它也是由Flask的作者开发的,Flask周边的Jinja、Werkzeug、flask_login都是由大佬一手开发orz

前面提到的四种全局变量概念就是用它实现的,比如在定义一个路由函数时,我们不需要为函数传入request变量,而可以直接由全局变量request(其实是request的代理)获取到我们当前请求的数据

# handle in Flask
@app.route("/")
def index():
    return "x"


# handle in other web framework, such as Django
def index(request):
    return HttpResponse("x")

看一下Flask源码,这些全局变量是什么东西:

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app


# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

_request_ctx_stack_app_ctx_stackLocalStack对象,衍生的四种全局变量都是对上下文对象某个成员的代理,接下来看werkzeug/local里的具体实现


Local类

class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

首先,__slots__限定了实例只有两个属性'__storage__', '__ident_func__'

看到__init__函数中是采用了object.__setattr__(self, x, x)来赋值,而不是直接self.x = xxsetattr(self, x, x)原因是重写了类的__setattr__方法后,这里想要调用原生的赋值行为(绕过重写的魔术方法),所以使用了这样的写法

初始化函数为对象创建了__storage__字典和__ident_func__方法,默认为threading.get_ident(获取当前线程id),当然,假如是greenlet之类的协程的话是可以修改__ident_func__,使其获取协程id来实现协程间数据隔离

__call__魔术方法以实例自身为参数构造了一个LocalProxy对象,待会看到LP类的时候再说

所以可看出Local对象实现了一个线程隔离的哈希表,它的核心是__storage__,结构是:

{
    `[thread id 1]: int`: {key: value},
    `[thread id 2]: int`: {key: value},
}

在不同线程中向Local对象set一个相同的key值,它的存储也是隔离的

__release_local__方法的作用是释放当前线程的所有数据,即直接pop当前线程id


LocalStack类

class LocalStack(object):

    """This class works similar to a :class:`Local` but keeps a stack
    of objects instead.  This is best explained with an example::

        >>> ls = LocalStack()
        >>> ls.push(42)
        >>> ls.top
        42
        >>> ls.push(23)
        >>> ls.top
        23
        >>> ls.pop()
        23
        >>> ls.top
        42

    They can be force released by using a :class:`LocalManager` or with
    the :func:`release_local` function but the correct way is to pop the
    item from the stack after using.  When the stack is empty it will
    no longer be bound to the current context (and as such released).

    By calling the stack without arguments it returns a proxy that resolves to
    the topmost item on the stack.

    .. versionadded:: 0.6.1
    """

    def __init__(self):
        self._local = Local()

    def __release_local__(self):
        self._local.__release_local__()

    def _get__ident_func__(self):
        return self._local.__ident_func__

    def _set__ident_func__(self, value):
        object.__setattr__(self._local, '__ident_func__', value)
    __ident_func__ = property(_get__ident_func__, _set__ident_func__)
    del _get__ident_func__, _set__ident_func__

    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)

    def push(self, obj):
        """Pushes a new item to the stack"""
        rv = getattr(self._local, 'stack', None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        """Removes the topmost item from the stack, will return the
        old value or `None` if the stack was already empty.
        """
        stack = getattr(self._local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

LocalStack类其实就是将Local封装了一层,在Local__storage__哈希表里新增了{'stack': []}作栈

首先为实例赋值了一个Local对象,接着将成员Local对象的__ident_func__property装饰为getter/setter,也就相当于将成员对象的属性提升到了自身,因为LocalStack类也需要接口来修改自身的__ident_func__来实现除线程外的数据安全。P.s. 这里少了个空行,排版看起来怪怪的,我刚开始还以为是个IndentError

接着它的push/pop也就实现了一个栈(以list实现),即LocalStack.Local.__storage__[thread_id]["stack"] = [],注意这里push的赋值: self._local.stack = rv = [],因为list是可变容器,所以下面的append方法实际修改了两个变量(同一对象的引用),而pop方法中,假如pop当前值后栈为空,则会直接release当前线程id的kv。

而这里需要留意的是,Local__getattr__方法,当字典的键不存在时抛出异常,而我们看到在push方法的第一次初始化时会getattr,但它却正常执行,原因在于,当reflet的getattr函数设置default参数时会recover抛出的AttributeError,见说明When a default argument is given, it is returned when the attribute doesn't exist; without it, an exception is raised in that case.


LocalManager

LocalManager的作用是将Local/LocalStack对象进行封装聚合,实现了修改get_ident以及增加中间件的接口,很简单的功能:

class LocalManager(object):

    """Local objects cannot manage themselves. For that you need a local
    manager.  You can pass a local manager multiple locals or add them later
    by appending them to `manager.locals`.  Every time the manager cleans up,
    it will clean up all the data left in the locals for this context.

    The `ident_func` parameter can be added to override the default ident
    function for the wrapped locals.

    .. versionchanged:: 0.6.1
       Instead of a manager the :func:`release_local` function can be used
       as well.

    .. versionchanged:: 0.7
       `ident_func` was added.
    """

    def __init__(self, locals=None, ident_func=None):
        if locals is None:
            self.locals = []
        elif isinstance(locals, Local):
            self.locals = [locals]
        else:
            self.locals = list(locals)
        if ident_func is not None:
            self.ident_func = ident_func
            for local in self.locals:
                object.__setattr__(local, '__ident_func__', ident_func)
        else:
            self.ident_func = get_ident

    def get_ident(self):
        """Return the context identifier the local objects use internally for
        this context.  You cannot override this method to change the behavior
        but use it to link other context local objects (such as SQLAlchemy's
        scoped sessions) to the Werkzeug locals.

        .. versionchanged:: 0.7
           You can pass a different ident function to the local manager that
           will then be propagated to all the locals passed to the
           constructor.
        """
        return self.ident_func()

    def cleanup(self):
        """Manually clean up the data in the locals for this context.  Call
        this at the end of the request or use `make_middleware()`.
        """
        for local in self.locals:
            release_local(local)

    def make_middleware(self, app):
        """Wrap a WSGI application so that cleaning up happens after
        request end.
        """
        def application(environ, start_response):
            return ClosingIterator(app(environ, start_response), self.cleanup)
        return application

    def middleware(self, func):
        """Like `make_middleware` but for decorating functions.

        Example usage::

            @manager.middleware
            def application(environ, start_response):
                ...

        The difference to `make_middleware` is that the function passed
        will have all the arguments copied from the inner application
        (name, docstring, module).
        """
        return update_wrapper(self.make_middleware(func), func)

    def __repr__(self):
        return '<%s storages: %d>' % (
            self.__class__.__name__,
            len(self.locals)
        )

LocalProxy

前面Local类和LocalStack类的__call__魔术方法:

# class Local
def __call__(self, proxy):
    """Create a proxy for a name."""
    return LocalProxy(self, proxy)

# class LocalStack
def __call__(self):
    def _lookup():
        rv = self.top
        if rv is None:
            raise RuntimeError('object unbound')
            return rv
    return LocalProxy(_lookup)

这里的LocalStack是通过闭包将自身实例包裹进一个lookup函数并返回

LocalProxy的实现:

@implements_bool
class LocalProxy(object):

    """Acts as a proxy for a werkzeug local.  Forwards all operations to
    a proxied object.  The only operations not supported for forwarding
    are right handed operands and any kind of assignment.

    Example usage::

        from werkzeug.local import Local
        l = Local()

        # these are proxies
        request = l('request')
        user = l('user')


        from werkzeug.local import LocalStack
        _response_local = LocalStack()

        # this is a proxy
        response = _response_local()

    Whenever something is bound to l.user / l.request the proxy objects
    will forward all operations.  If no object is bound a :exc:`RuntimeError`
    will be raised.

    To create proxies to :class:`Local` or :class:`LocalStack` objects,
    call the object as shown above.  If you want to have a proxy to an
    object looked up by a function, you can (as of Werkzeug 0.6.1) pass
    a function to the :class:`LocalProxy` constructor::

        session = LocalProxy(lambda: get_current_request().session)

    .. versionchanged:: 0.6.1
       The class can be instantiated with a callable as well now.
    """
    __slots__ = ('__local', '__dict__', '__name__', '__wrapped__')

    def __init__(self, local, name=None):
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)
        if callable(local) and not hasattr(local, '__release_local__'):
            # "local" is a callable that is not an instance of Local or
            # LocalManager: mark it as a wrapped function.
            object.__setattr__(self, '__wrapped__', local)

    def _get_current_object(self):
        """Return the current object.  This is useful if you want the real
        object behind the proxy at a time for performance reasons or because
        you want to pass the object into a different context.
        """
        if not hasattr(self.__local, '__release_local__'):
            return self.__local()
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.__name__)

    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')

    def __repr__(self):
        try:
            obj = self._get_current_object()
        except RuntimeError:
            return '<%s unbound>' % self.__class__.__name__
        return repr(obj)

    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False

    def __unicode__(self):
        try:
            return unicode(self._get_current_object())  # noqa
        except RuntimeError:
            return repr(self)

    def __dir__(self):
        try:
            return dir(self._get_current_object())
        except RuntimeError:
            return []

    def __getattr__(self, name):
        if name == '__members__':
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)

    def __setitem__(self, key, value):
        self._get_current_object()[key] = value

    def __delitem__(self, key):
        del self._get_current_object()[key]

    if PY2:
        __getslice__ = lambda x, i, j: x._get_current_object()[i:j]

        def __setslice__(self, i, j, seq):
            self._get_current_object()[i:j] = seq

        def __delslice__(self, i, j):
            del self._get_current_object()[i:j]

    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
    __delattr__ = lambda x, n: delattr(x._get_current_object(), n)
    __str__ = lambda x: str(x._get_current_object())
    __lt__ = lambda x, o: x._get_current_object() < o
    __le__ = lambda x, o: x._get_current_object() <= o
    __eq__ = lambda x, o: x._get_current_object() == o
    __ne__ = lambda x, o: x._get_current_object() != o
    __gt__ = lambda x, o: x._get_current_object() > o
    __ge__ = lambda x, o: x._get_current_object() >= o
    __cmp__ = lambda x, o: cmp(x._get_current_object(), o)  # noqa
    __hash__ = lambda x: hash(x._get_current_object())
    __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw)
    __len__ = lambda x: len(x._get_current_object())
    __getitem__ = lambda x, i: x._get_current_object()[i]
    __iter__ = lambda x: iter(x._get_current_object())
    __contains__ = lambda x, i: i in x._get_current_object()
    __add__ = lambda x, o: x._get_current_object() + o
    __sub__ = lambda x, o: x._get_current_object() - o
    __mul__ = lambda x, o: x._get_current_object() * o
    __floordiv__ = lambda x, o: x._get_current_object() // o
    __mod__ = lambda x, o: x._get_current_object() % o
    __divmod__ = lambda x, o: x._get_current_object().__divmod__(o)
    __pow__ = lambda x, o: x._get_current_object() ** o
    __lshift__ = lambda x, o: x._get_current_object() << o
    __rshift__ = lambda x, o: x._get_current_object() >> o
    __and__ = lambda x, o: x._get_current_object() & o
    __xor__ = lambda x, o: x._get_current_object() ^ o
    __or__ = lambda x, o: x._get_current_object() | o
    __div__ = lambda x, o: x._get_current_object().__div__(o)
    __truediv__ = lambda x, o: x._get_current_object().__truediv__(o)
    __neg__ = lambda x: -(x._get_current_object())
    __pos__ = lambda x: +(x._get_current_object())
    __abs__ = lambda x: abs(x._get_current_object())
    __invert__ = lambda x: ~(x._get_current_object())
    __complex__ = lambda x: complex(x._get_current_object())
    __int__ = lambda x: int(x._get_current_object())
    __long__ = lambda x: long(x._get_current_object())  # noqa
    __float__ = lambda x: float(x._get_current_object())
    __oct__ = lambda x: oct(x._get_current_object())
    __hex__ = lambda x: hex(x._get_current_object())
    __index__ = lambda x: x._get_current_object().__index__()
    __coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o)
    __enter__ = lambda x: x._get_current_object().__enter__()
    __exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw)
    __radd__ = lambda x, o: o + x._get_current_object()
    __rsub__ = lambda x, o: o - x._get_current_object()
    __rmul__ = lambda x, o: o * x._get_current_object()
    __rdiv__ = lambda x, o: o / x._get_current_object()
    if PY2:
        __rtruediv__ = lambda x, o: x._get_current_object().__rtruediv__(o)
    else:
        __rtruediv__ = __rdiv__
    __rfloordiv__ = lambda x, o: o // x._get_current_object()
    __rmod__ = lambda x, o: o % x._get_current_object()
    __rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o)
    __copy__ = lambda x: copy.copy(x._get_current_object())
    __deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)

首先,可以知道实例属性被限制了,而这里可以看到在__slots__中又添加了__dict__属性,看起来是矛盾的,也就是说其实还是能随意为实例新增属性的。但其实这里的目的是为了能让Proxy对象转发被代理obj的__dict__属性,所以用property装饰器将方法转为属性,然后在__slots__里添加了__dict__

__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')

@property
def __dict__(self):
    try:
        return self._get_current_object().__dict__
    except RuntimeError:
        raise AttributeError('__dict__')

初始化函数:

def __init__(self, local, name=None):
    object.__setattr__(self, '_LocalProxy__local', local)
    object.__setattr__(self, '__name__', name)
    if callable(local) and not hasattr(local, '__release_local__'):
        # "local" is a callable that is not an instance of Local or
        # LocalManager: mark it as a wrapped function.
        object.__setattr__(self, '__wrapped__', local)

必填参数local,赋值给_LocalProxy__local,也就是__local,私有变量的类外名称会转化为_classname__funcname,因为调用的基类的__setatr__方法,所以需要用它的类外名称。接着判断local参数是否存在__call__魔术方法且无__release_local__,也就是说这里Local实例不会被赋值给__wrapped__,只有通过call LocalStack或自建的函数会被添加装饰标记

代理的核心——_get_current_obj

def _get_current_object(self):
    """Return the current object.  This is useful if you want the real
        object behind the proxy at a time for performance reasons or because
        you want to pass the object into a different context.
        """
    if not hasattr(self.__local, '__release_local__'):
        return self.__local()
    try:
        return getattr(self.__local, self.__name__)
    except AttributeError:
        raise RuntimeError('no object bound to %s' % self.__name__)

首先判断是否为普通lookup函数,如果是则直接调用返回。如是Local实例,则通过实例重写后的__getattr__获取name对应的值

总结它的行为:如果是LocalStack实例化的代理,则返回顶部元素;如果是Local实例化的,则返回__storage__字典里实例化时__name__对应的变量。也就是说,代理只代理一个对象(或栈的top)

剩下的部分就是重写了几乎所有的魔术方法,将它完全变成了一个代理对象,所有的操作符/运算符都是针对它所代理的local元素

代理的意义:

为什么需要在Local/LocalStack前加一层代理?其实这是设计模式里的代理模式

假如说我们写这样的代码:

s = LocalStack()
s.push(obj1)
s.push(obj2)

def get_obj():
    return s.pop()

# 1
p = LocalProxy(get_obj)
print(p)
print(p)

# 2
p = get_obj()
# or
p = s.top

第一种方法和第二种方法是不一样的,代理模式在每一次访问代理对象的时候都会动态获取,而不增代理的话,赋值后就无法再次动态获取了,当然用函数来获取也是可以的,但它就不太像操作对象的属性值了


了解werkzeug自己实现的Local类后可以回来看Flask的上下文实现了

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

请求上下文和程序上下文都是LocalStack实例,也就是线程(协程)安全的栈结构,四种全局变量:

current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

四种全局变量构造函数的实参就是一个能获取前面两种栈的栈顶元素的函数:

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app

这里分析一下RequestContextAPPContext和前者其实是差不多的

初始化函数:

def __init__(self, app, environ, request=None):
    self.app = app
    if request is None:
        request = app.request_class(environ)
    self.request = request
    self.url_adapter = app.create_url_adapter(self.request)
    self.flashes = None
    self.session = None

    # Request contexts can be pushed multiple times and interleaved with
    # other request contexts.  Now only if the last level is popped we
    # get rid of them.  Additionally if an application context is missing
    # one is created implicitly so for each level we add this information
    self._implicit_app_ctx_stack = []

    # indicator if the context was preserved.  Next time another context
    # is pushed the preserved context is popped.
    self.preserved = False

    # remembers the exception for pop if there is one in case the context
    # preservation kicks in.
    self._preserved_exc = None

    # Functions that should be executed after the request on the response
    # object.  These will be called before the regular "after_request"
    # functions.
    self._after_request_functions = []

    self.match_request()

需要一个Flask app实例作实参,以及wsgienvironment,下面是g变量

def _get_g(self):
    return _app_ctx_stack.top.g
def _set_g(self, value):
    _app_ctx_stack.top.g = value
g = property(_get_g, _set_g)
del _get_g, _set_g

property设置了ggetter/setterg是从app上下文栈的栈顶的上下文对象里取得的

看一下AppContextg的定义:

self.g = app.app_ctx_globals_class()

进入Flask类看一看app_ctx_globals_class

#: In Flask 0.9 this property was called `request_globals_class` but it
#: was changed in 0.10 to :attr:`app_ctx_globals_class` because the
#: flask.g object is now application context scoped.
#:
#: .. versionadded:: 0.10
app_ctx_globals_class = _AppCtxGlobals

可见它是一个类,回到flask/ctx查看类定义:

class _AppCtxGlobals(object):
    """A plain object. Used as a namespace for storing data during an
    application context.

    Creating an app context automatically creates this object, which is
    made available as the :data:`g` proxy.

    .. describe:: 'key' in g

        Check whether an attribute is present.

        .. versionadded:: 0.10

    .. describe:: iter(g)

        Return an iterator over the attribute names.

        .. versionadded:: 0.10
    """

    def get(self, name, default=None):
        """Get an attribute by name, or a default value. Like
        :meth:`dict.get`.

        :param name: Name of attribute to get.
        :param default: Value to return if the attribute is not present.

        .. versionadded:: 0.10
        """
        return self.__dict__.get(name, default)

    def pop(self, name, default=_sentinel):
        """Get and remove an attribute by name. Like :meth:`dict.pop`.

        :param name: Name of attribute to pop.
        :param default: Value to return if the attribute is not present,
            instead of raise a ``KeyError``.

        .. versionadded:: 0.11
        """
        if default is _sentinel:
            return self.__dict__.pop(name)
        else:
            return self.__dict__.pop(name, default)

    def setdefault(self, name, default=None):
        """Get the value of an attribute if it is present, otherwise
        set and return a default value. Like :meth:`dict.setdefault`.

        :param name: Name of attribute to get.
        :param: default: Value to set and return if the attribute is not
            present.

        .. versionadded:: 0.11
        """
        return self.__dict__.setdefault(name, default)

    def __contains__(self, item):
        return item in self.__dict__

    def __iter__(self):
        return iter(self.__dict__)

    def __repr__(self):
        top = _app_ctx_stack.top
        if top is not None:
            return '<flask.g of %r>' % top.app.name
        return object.__repr__(self)

g变量是通过__dict__设置实例属性来保存数据的

从1.0开始,g变量存储在app上下文里,而不是请求上下文里,但它依然会在每一次请求重置,所以作用没有变:在一次请求间共享数据

#: In Flask 0.9 this property was called `request_globals_class` but it
#: was changed in 0.10 to :attr:`app_ctx_globals_class` because the
#: flask.g object is now application context scoped.

网上有人说之所以g只在一次请求间可共享,是因为每次请求会重置g变量,这是错误的,因为我们可以看到g的实例化是在AppContext的初始化函数中,而却没有留重置的接口,所以我们尝试打印每一次请求的app上下文id:

from flask import Flask, _app_ctx_stack

app = Flask(__name__)

@app.route("/")
def index():
    return str(id(_app_ctx_stack.top))

@app.route("/s")
def s():
    return str(id(_app_ctx_stack.top))

app.run()

运行即可知道,每一次请求的AppContext都是不同的对象,所以说,其实每一次的请求都创建了一个新的AppContext,也就是在app.wsgi_app中,由RequestContext来隐式实例化AppContext并压栈,而当一次请求结束时,app上下文栈和请求上下文栈都会被pop,这里在最后再分析

继续看请求上下文push方法:

def push(self):
    """Binds the request context to the current context."""
    # If an exception occurs in debug mode or if context preservation is
    # activated under exception situations exactly one context stays
    # on the stack.  The rationale is that you want to access that
    # information under debug situations.  However if someone forgets to
    # pop that context again we want to make sure that on the next push
    # it's invalidated, otherwise we run at risk that something leaks
    # memory.  This is usually only a problem in test suite since this
    # functionality is not active in production environments.
    top = _request_ctx_stack.top
    if top is not None and top.preserved:
        top.pop(top._preserved_exc)

    # Before we push the request context we have to ensure that there
    # is an application context.
    app_ctx = _app_ctx_stack.top
    if app_ctx is None or app_ctx.app != self.app:
        app_ctx = self.app.app_context()
        app_ctx.push()
        self._implicit_app_ctx_stack.append(app_ctx)
    else:
        self._implicit_app_ctx_stack.append(None)

    if hasattr(sys, 'exc_clear'):
        sys.exc_clear()

    _request_ctx_stack.push(self)

    # Open the session at the moment that the request context is available.
    # This allows a custom open_session method to use the request context.
    # Only open a new session if this is the first time the request was
    # pushed, otherwise stream_with_context loses the session.
    if self.session is None:
        session_interface = self.app.session_interface
        self.session = session_interface.open_session(
            self.app, self.request
        )

        if self.session is None:
            self.session = session_interface.make_null_session(self.app)

看到它会确认当前AppContext不为空,且栈顶上下文的app为自身app,接着将自身实例压入_request_ctx_stack

pop方法里:

def pop(self, exc=_sentinel):
    """Pops the request context and unbinds it by doing that.  This will
    also trigger the execution of functions registered by the
    :meth:`~flask.Flask.teardown_request` decorator.

    .. versionchanged:: 0.9
       Added the `exc` argument.
    """
    app_ctx = self._implicit_app_ctx_stack.pop()

    try:
        clear_request = False
        if not self._implicit_app_ctx_stack:
            self.preserved = False
            self._preserved_exc = None
            if exc is _sentinel:
                exc = sys.exc_info()[1]
            self.app.do_teardown_request(exc)

            # If this interpreter supports clearing the exception information
            # we do that now.  This will only go into effect on Python 2.x,
            # on 3.x it disappears automatically at the end of the exception
            # stack.
            if hasattr(sys, 'exc_clear'):
                sys.exc_clear()

            request_close = getattr(self.request, 'close', None)
            if request_close is not None:
                request_close()
            clear_request = True
    finally:
        rv = _request_ctx_stack.pop()

        # get rid of circular dependencies at the end of the request
        # so that we don't require the GC to be active.
        if clear_request:
            rv.request.environ['werkzeug.request'] = None

        # Get rid of the app as well if necessary.
        if app_ctx is not None:
            app_ctx.pop(exc)

        assert rv is self, 'Popped wrong request context.  ' \
            '(%r instead of %r)' % (rv, self)

pop当前上下文时,会先执行teardown_request里的函数,这是Flask@teardown_request装饰器注册的hook函数,最后断言pop出的请求上下文is自身实例

查看flask/app的源码:

def app_context(self):
    """Create an :class:`~flask.ctx.AppContext`. Use as a ``with``
    block to push the context, which will make :data:`current_app`
    point at this application.

    An application context is automatically pushed by
    :meth:`RequestContext.push() <flask.ctx.RequestContext.push>`
    when handling a request, and when running a CLI command. Use
    this to manually create a context outside of these situations.

    ::

        with app.app_context():
            init_db()

    See :doc:`/appcontext`.

    .. versionadded:: 0.9
    """
    return AppContext(self)

def request_context(self, environ):
    """Create a :class:`~flask.ctx.RequestContext` representing a
    WSGI environment. Use a ``with`` block to push the context,
    which will make :data:`request` point at this request.

    See :doc:`/reqcontext`.

    Typically you should not call this from your own code. A request
    context is automatically pushed by the :meth:`wsgi_app` when
    handling a request. Use :meth:`test_request_context` to create
    an environment and context instead of this method.

    :param environ: a WSGI environment
    """
    return RequestContext(self, environ)

在请求到来时,werkzeug调用Flask实例,接着创建一个请求上下文,而假如此时app栈为空,RequestContext的push方法则会隐式实例化一个AppContext并压栈

def wsgi_app(self, environ, start_response):
    """The actual WSGI application. This is not implemented in
    :meth:`__call__` so that middlewares can be applied without
    losing a reference to the app object. Instead of doing this::

        app = MyMiddleware(app)

    It's a better idea to do this instead::

        app.wsgi_app = MyMiddleware(app.wsgi_app)

    Then you still have the original application object around and
    can continue to call methods on it.

    .. versionchanged:: 0.7
        Teardown events for the request and app contexts are called
        even if an unhandled error occurs. Other events may not be
        called depending on when an error occurs during dispatch.
        See :ref:`callbacks-and-errors`.

    :param environ: A WSGI environment.
    :param start_response: A callable accepting a status code,
        a list of headers, and an optional exception context to
        start the response.
    """
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            ctx.push()
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        except:
            error = sys.exc_info()[1]
            raise
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)

def __call__(self, environ, start_response):
    """The WSGI server calls the Flask application object as the
    WSGI application. This calls :meth:`wsgi_app` which can be
    wrapped to applying middleware."""
    return self.wsgi_app(environ, start_response)


最后总结几点:

  • 一般情况下AppContext的生命周期同RequestContext,甚至准确点说它的生命周期比请求上下文还要短一点。它只是作一个获取当前应用的代理。是随请求到来时与请求上下文一起创建的,而并不是很多人所想的它是当前应用的上下文

    直观点验证,我们修改Flask的源码,分别在AppContextRequestContext的初始化函数里打印debug信息,并定义析构函数,在析构函数里也打印debug信息,运行后的结果:

     * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    request ctx init
    app ctx init
    app ctx del
    request ctx del
    127.0.0.1 - - [20/Jan/2019 18:01:42] "GET / HTTP/1.1" 200 -
    request ctx init
    app ctx init
    app ctx del
    request ctx del
    127.0.0.1 - - [20/Jan/2019 18:02:42] "GET /s HTTP/1.1" 200 -
    

    可以清晰的看到它们的生命周期

  • 当请求到来时,由werkzeug调用Flask对象的__call__魔术方法,接着调用wsgi_app,创建一个请求上下文,接着将请求上下文push到请求上下文栈中,而它判断目前无应用上下文,就会隐式创建,并入栈,在请求结束后,会执行一系列hook函数,接着调用请求上下文的pop方法,此时会判断是否之前隐式创建了应用上下文,如是,则一起pop掉:

    # Request contexts can be pushed multiple times and interleaved with
    # other request contexts.  Now only if the last level is popped we
    # get rid of them.  Additionally if an application context is missing
    # one is created implicitly so for each level we add this information
    self._implicit_app_ctx_stack = []
      
    # ...
    def push(self):
        # ...
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)
              
    def pop(self, exc=_sentinel):
        """Pops the request context and unbinds it by doing that.  This will
            also trigger the execution of functions registered by the
            :meth:`~flask.Flask.teardown_request` decorator.
      
            .. versionchanged:: 0.9
               Added the `exc` argument.
            """
        app_ctx = self._implicit_app_ctx_stack.pop()
          
        # ...
        if app_ctx is not None:
            app_ctx.pop(exc)
    
  • 栈的意义,因为Local对象已经线程(协程)间数据隔离了,所以只需讨论单线程的情况。当单线程请求经历多个中间件时,AppContext一层一层压栈,能保证获取到的总是目前处理的app;而对于请求上下文,在写应用时不会有这个情况,之所以用栈结构是为了在写测试或离线脚本时手动with app.request_context()时能数据隔离