flask核心机制:current_app

1、 flask中经典错误 working outside application context

错误:
working outside application contex
原因:
在没有获取到应用上下文的情况下,进行了上下文操作。
代码:

from flask import Flask, current_app

app = Flask(__name__)

a = current_app
d = current_app.config['DEBUG']

运行:

image

2、 AppContext、RequestContext、Flask与Request之间的关系

  1. AppContext
    应用上下文,是对flask一切对象的封装
  2. RequestContext
    请求上下文,是对request请求对象的封装
  3. current_app
    类型是LocalProxy
    像全局变量一样工作,但只能在处理请求期间且在处理它的线程中访问
    返回的栈顶元素不是应用上下文,而是flask的应用实例对象

应用上下文的封装=flask核心对象+和外部协作对象(再flask封装对象上再添加push、pop等)(请求上下文同理)
代码:
F12进入current_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'))

current_app与reques:
current_app和reques都是设计模式中代理设计的代理对象,指向flask核心对象和reques的请求类

3、详解flask上下文与出入栈

这里写图片描述

在pycharm的flask项目中可以通过:
External Libraries->site-packages->flask->ctx.py
可以看到源码的实现
ctx.py中有AppContextRequestContext两个函数,都实现了push()pop()
AppContext

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)

flask在RequestContext入栈前会检查另外一个AppContext的栈的情况,如果栈顶元素为空或者不是当前对象,就会把AppContext推入栈中,然后RequestContext才进栈。
例如:
过程就好比导游与游客。

  • 导游->AppContext
  • 游客->RequestContext
  • 工作->push
  • 消费->push
  • 向导->current_app
  • 游玩->request
  • 旅程->LocalStack

每批游客都需要一位导游作为向导,人生地不熟如果没有向导就麻烦了,因此游客开始去游玩前需要有导游带团。在这旅程中,导游和游客虽然分别是工作和消费,但导游(对象:栈顶元素)的任务就是给游客提供向导(属性:app),不提供其他服务,而游客负责游玩。当游客结束这旅程的同时导游的任务也完成(两个栈中的元素会被弹出)了。

手动AppContext进栈

代码:

from flask import Flask, current_app

app = Flask(__name__)

ctx = app.app_context()
ctx.push()
a = current_app
ctx.pop()

运行:

image

注意:
但最终current_app返回的栈顶元素不是应用上下文,而是flask的应用实例对象!
F12进入查看源码

current_app = LocalProxy(_find_app)

查看_find_app函数

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

可以发现最后返回的是top对象中的app。
reques和session同理,不过传多一个字符串进行查找。

flask自动入栈

如果是在一个请求中直接使用current_app对象是不用手动把AppContext推入栈中的。如之前所说RequestContext入栈前会检查另外一个AppContext的栈的情况,这个操作会由flask帮你完成。

手动进栈存在的价值

单元测试不在reques请求环境中执行,需要手动AppContext进栈。离线应用。例如待会介绍的异步邮箱例子。

4、flask上下文与with语句

可以使用with 来实现自动入栈和出栈,比上面手动push、pop的更优雅,因为在AppContext中已实现两个特殊方法enterexit,也被称为“魔法方法”,凡是实现了这两个特殊方法的对象都可以被with所使用。

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

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

python with的使用这里不详细讨论,在flask中注意的是:

  • 实现了上下文协议的对象使用with
  • with被称做上下文管理器
  • 只要实现enterexit就是实现上下文协议
  • 上下文表达式(app.app_context())必须返回一个上下文管理器(AppContext)
  • exit最后三个参数记录发生异常时的信息,exit返回bool类型,返回true表示正常,false会抛出异常,没有返回值默认为false

代码:

from flask import Flask, current_app

app = Flask(__name__)

with app.app_context():
    a = current_app

运行:

image

上图中全局变量a在with的作用域中的值为flask应用的实例对象main,当with关闭后就变成了LocalProxy unbound

5、flask中实际应用

发送密码重置电子邮件

from flask import render_template
from app import app

# ...

def send_password_reset_email(user):
    """
    令牌,生成密码重置电子邮件
    """
    token = user.get_reset_password_token()
    # 调用email.py中的send_email函数
    send_email('[Microblog] Reset Your Password',
               sender=app.config['ADMINS'][0],
               recipients=[user.email],
               text_body=render_template('email/reset_password.txt',
                                         user=user, token=token),
               html_body=render_template('email/reset_password.html',
                                         user=user, token=token))

异步电子邮件

email.py:

from threading import Thread
from flask import current_app
from flask_mail import Message
from app import mail


def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def send_email(subject, sender, recipients, text_body, html_body,
               attachments=None, sync=False):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    if attachments: # 附件
        for attachment in attachments:
            msg.attach(*attachment)
    if sync: # 是否异步
        mail.send(msg)
    else:
        Thread(target=send_async_email,
            args=(current_app._get_current_object(), msg)).start()

1. send_async_email()

mail.send()方法需要访问电子邮件服务器的配置值,而这必须通过访问应用属性的方式来实现。 使用with app.app_context()调用创建的应用上下文使得应用实例可以通过来自Flask的current_app变量来进行访问。

2. send_email():

send_email()函数中,应用实例作为参数传递给后台线程,后台线程将发送电子邮件而不阻塞主应用程序。在作为后台线程运行的send_async_email()函数中直接使用current_app将不会奏效,因为current_app是一个与处理客户端请求的线程绑定的上下文感知变量。在另一个线程中,current_app没有赋值。直接将current_app作为参数传递给线程对象也不会有效,因为current_app实际上是一个代理对象,它被动态地映射到应用实例。因此,传递代理对象与直接在线程中使用current_app相同。我需要做的是访问存储在代理对象中的实际应用程序实例,并将其作为app参数传递。 current_app._get_current_object()表达式从代理对象中提取实际的应用实例,所以它就是我作为参数传递给线程的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容