flask源码解析之上下文为什么用栈

2020-01-19 10:52:20 浏览数 (1)

楔子

我在之前的文章《flask源码解析之上下文》中对flask上下文流程进行了详细的说明,但是在学习的过程中我一直在思考flask上下文中为什么要使用栈完成对请求上下文和应用上下文的入栈和出栈操作,而且栈所维护的无非不就是一个列表,我直接用一个列表去存储请求上下文和应用上下文不可以吗?或者说我用一个变量、字典其他任何可存储数据的数据类型不行吗?对于这个问题的解答,是我在理解离线脚本和 flask多app应用中才理解flask上下文中使用栈的精髓。对于为什么使用栈进行存储上下文,请耐心看我之前对离线脚本和flask多app应用的铺垫。

离线脚本

在项目的实际应用中,我们需要使用离线脚本完成不能作为后台功能的操作,例如:

    1. 每天凌晨对还款用户进行短信提醒

    2. 每天凌晨对数据库进行特定操作

    3. 在项目开发完成交付他人进行测试之前,使用一个脚本完成对数据库的初始化操作

  对于以上的所要实现的功能,他们都不能作为后台代码开发的一部分,只能使用一个离线的脚本完成上述操作。我们以需求2为例编写一个离线脚本:

点我下载demo项目

代码语言:javascript复制
from sansa import db
from sansa.models import Users
db.session.add(Users(name="大萨达所"))
db.session.commit()

在项目未启动情况下,运行此脚本抛出以下错误:

代码语言:javascript复制
"D:Program FilesPython36python.exe" D:/Demo/s8/demo/sansa/数据插入的离线脚本.py
Traceback (most recent call last):
  File "D:Program FilesPython36libsite-packagessqlalchemyutil_collections.py", line 999, in __call__
    return self.registry[key]
KeyError: <greenlet.greenlet object at 0x0000000003723930>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:/Demo/s8/demo/sansa/数据插入的离线脚本.py", line 3, in <module>
    db.session.add(Users(name="大萨达所"))
  File "D:Program FilesPython36libsite-packagessqlalchemyormscoping.py", line 158, in do
    return getattr(self.registry(), name)(*args, **kwargs)
  File "D:Program FilesPython36libsite-packagessqlalchemyutil_collections.py", line 1001, in __call__
    return self.registry.setdefault(key, self.createfunc())
  File "D:Program FilesPython36libsite-packagessqlalchemyormsession.py", line 2950, in __call__
    return self.class_(**local_kw)
  File "D:Program FilesPython36libsite-packagesflask_sqlalchemy__init__.py", line 141, in __init__
    self.app = app = db.get_app()
  File "D:Program FilesPython36libsite-packagesflask_sqlalchemy__init__.py", line 912, in get_app
    'No application found. Either work inside a view function or push'
RuntimeError: No application found. Either work inside a view function or push an application context. See http://flask-sqlalchemy.pocoo.org/contexts/.

Process finished with exit code 1

提示错误的原因是没有找到应用上下文对象的入栈操作,出现此问题的原因是:我们只是导入了db,但是项目原项目没有启动,更没有请求到来一说,我们执行的此离线脚本从头到尾就没有出现应用上下文,那么就更不会有应用上下文的入栈和出栈操作,所以db找到不到应用上下文,找不到应用上下文就无法导入连接数据库的配置信息。现在既然我们知道了出现问题的原因,解决此问题的方法就是我们手动执行应用上下文的创建、入栈和出栈操作,即要执行以下步骤:

1. app_ctx = self.app.app_context()

2.  app_ctx.push()

3.  app_ctx.pop(exc)

因此,编写的离线脚本为:

代码语言:javascript复制
from sansa import db,create_app
from sansa.models import Users

# 获取到生成app
app = create_app().app
# 创建app_ctx
app_ctx = app.app_context()
# app_ctx 入栈
app_ctx.push()
# 对数据库进行操作
db.session.add(Users(name="大萨达所"))
# 提交
db.session.commit()
app_ctx.pop()

由于 app_ctx 有  __enter__ 和  __exit__ 方法:

代码语言:javascript复制
 def __enter__(self):
        self.push()
        return self
代码语言:javascript复制
    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)

根据以上方法,离线脚本还可以这样写:

代码语言:javascript复制
from sansa import db,create_app
from sansa.models import Users
app = create_app().app

with app.app_context():
    # 对数据库进行操作
    db.session.add(Users(name="大萨达所"))
    # 提交
    db.session.commit()
    # 释放当前的连接
    db.session.remove()

这样,此脚本可实现在项目离线的情况下,按照指定的时间点执行该离线脚本完成对数据库的操作。

 flask的多app应用

博主有在《flask源码解析之DispatcherMiddleware》一文中对flask多app应用的使用和源码流程进行过详细阐述,flask多app所实现的功能与蓝图相同,就是完成路由分发的功能。例如:

代码语言:javascript复制
from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.serving import run_simple
from flask import Flask, current_app,request

app1 = Flask('app01')

app2 = Flask('app02')


@app1.route('/index/')
def index():
    return "app01"


@app2.route('/index2/')
def index2():
    """
    存在的问题:
            def __call__(self, environ, start_response):
        # 获取当前请求的URL,script == '/index/'
        script = environ.get('PATH_INFO', '')
        path_info = ''
        # '/' 在 '/index/' 中
        while '/' in script:
            # self.mounts == {'/sec': app2,}, '/' 不在 self.mounts 中
            if script in self.mounts:
                app = self.mounts[script]
                break
            script, last_item = script.rsplit('/', 1)
            path_info = '/%s%s' % (last_item, path_info)
        else:
            app = self.mounts.get(script, self.app)
        original_script_name = environ.get('SCRIPT_NAME', '')
        environ['SCRIPT_NAME'] = original_script_name   script
        environ['PATH_INFO'] = path_info  #  这里得到的知识不包含前缀的url,那这样不是就丢失了吗
        return app(environ, start_response)
    :return:
    """
    print(request.full_path) # 得到的是 /index2/? 且没有request.path_info
    return "app2"


# http://www.oldboyedu.com/index
# http://www.oldboyedu.com/sec/index2
dm = DispatcherMiddleware(app1, {
    '/sec': app2,
})

if __name__ == "__main__":
    run_simple('localhost', 5000, dm)

为什么用栈

在 执行离线脚本 多app应用下,执行如下脚本程序:

代码语言:javascript复制
from sansa import db,create_app
from sansa.models import Users
app1 = create_app1().app
app2 = create_app2().app

with app1.app_context():
    # 对数据库进行操作
    db.session.add(Users(name="大萨达所"))
    # 提交
    db.session.commit()
    # 释放当前的连接
    db.session.remove()
    with app2.app_context():
        db.session.add(Users(name="xxx"))
        # 提交
        db.session.commit()
        # 释放当前的连接
        db.session.remove()

对于上述离线脚本程序,入栈顺序为: app1.app_context() --->  app2.app_context() ,由于执行当前脚本程序只开了一个线程,因此在两个应用上下文入栈的时候会存放至同一个线程id所维护的栈中;出栈的时候按照当前线程id去匹配此线程id所对维护的栈,每一个应用上下文在出栈的时候都会获取栈顶元素,即出栈顺序为: app2.app_context() --->  app1.app_context() ,完成了后进先出的栈特点,因此需要栈的数据结构。

0 人点赞