Flask Signals详解

Flask Signals简介

Flask Signals和操作系统的signals系统很类似,都是通过信号(也可以说是事件event)来通知已经注册的回调函数,让回调函数自动开始执行。Flask定义了自己的一套核心signals和对应的functions(用于发起消息,注册回调函数),我们需要定义自己的回调函数,然后注册到对应的signal,这样就可以在收到该信号的时候自动执行我们定义的回调函数。

什么情况下需要使用Signals?

当我们需要使用观察者模式来解耦模块之间的信息传递的时候,Signals系统就可以帮助我们轻松达到目的。观察者模式如下图(图片来自voidcn)


观察者模式

与Hook函数的区别

试想,当我们需要监听某个事件,当它发生的时候,需要执行一系列functions,来实现诸如log记录等功能时,我们就可以使用Signals系统来实现,但是这里有一个疑问就是这个功能通过hook函数似乎也可以实现,比如通过before_request decorator实现记录日志的功能和使用request_started来记录日志就非常相似, 如下代码所示:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask, request, request_started
app = Flask(__name__)

@app.before_request
def print_url_in_hook():
    print "in hook, url: %s" % request.url

@app.route("/")
def hello():
    return "Hello, World!"

def print_url_in_signal_subscriber(sender, **extra):
    print "in signal subscriber, url: %s" % request.url

if __name__ == "__main__":
    request_started.connect(print_url_in_signal_subscriber, app)
    app.run()

当收到http请求后,打印如下:

in signal subscriber, url: http://localhost:5000/
in hook, url: http://localhost:5000/
127.0.0.1 - - [05/Oct/2017 16:57:20] "GET / HTTP/1.1" 200 -

那么到底什么情况下使用signal,什么情况下使用hook函数呢?我们来看下它们的主要区别:

  1. signal的callback函数是无顺序的,而hook函数的执行是按照定义的顺序执行的。(这一点虽然是官网提出的区别,但是实际测试发现signal执行实际是按照注册的顺序执行的,即先通过connect进行注册的回调函数会先被执行)
  2. signal无法直接abort这个request请求,相比较在hook函数中可以直接abort request,即直接返回response给客户端,而无需再执行后续的操作。
  3. signal可以通过参数携带数据,而hook函数通常不会携带额外的参数

与RabbitMQ等消息中间件的区别

Rabbitmq与signals都支持观察者模式,但是它们的区别也是很明显的:

  1. Rabbitmq之类的消息中间件更加重量级,提供更多功能,如分布式部署,消息存储备份等功能,而signal系统显然更加轻量级,只提供简单的消息分发功能
  2. Rabbitmq之类的消息中间件可以在不同的系统间传递消息,从而使得不同的功能模块可以使用不同的语言进行开发,而signal系统显然仅限于Flask系统中使用

显然,signal系统使用局限性更大,但也更加轻量级,在只是简单的进行消息分发的系统中,使用signal更加简单方便

怎么使用Signals?

Flask提供的signal机制优先使用blinker提供的库,但当blinker没有安装的时候,Flask也可以回退到使用自己的库。但是鉴于官网推荐使用blinker,所以我们最好还是安装blinker。

使用blinker

安装blinker

pip install blinker

测试Flask signal是否使用blinker

In [1]: from flask import signals

In [2]: signals.signals_available
Out[2]: True

signals.signals_available返回True时,说明使用的是Blinker库

使用Flask Built-in signals

Flask内置有多个signals可以直接使用,这些signals会自动emit(发射),我们只需要定义自己的回调函数,然后通过connect方式来subscribe我们定义的函数到对应的signal即可监听该signal

下表展示了Flask内置的Signals,详细请参考Flask built-in signals:

Signals 说明
template_rendered 当template被成功渲染之后会触发
before_render_template 当template被渲染之前会触发
request_started 当request context建立好之后,并在request被处理之前
request_finished 当发送response给客户端之后被触发
got_request_exception 当request处理过程中发生异常时,该signal会被触发,它甚至早于程序中的异常处理
request_tearing_down 当request tear down的时候触发,无论何种情况该signal都会被触发,即使发生异常
appcontext_tearing_down 当应用的context tear down的时候触发
appcontext_pushed 当应用的context被push时触发
appcontext_popped 当应用的context被pop时触发
message_flashed 当应用发送flash message时触发

之前的例子我们已经看到如何使用request_started signal了,这里需要说明两点:

  1. 在定义回调函数时,第一个参数必须是sender对象(即发送该signal的对象),第二个参数**extra用于接受额外的参数,也防止将来Flask在发送signal时添加新的参数。
  2. 使用connect注册回调函数时,第一个参数是回调函数,这个是必须的,第二参数是sender对象,是可选的,但最佳实践是要明确发送该signal的对象

另外,我们也可以临时性注册一个回调函数,这个尤其在进行单元测试时非常有用,因为我们不想在实际程序中添加测试相关的回调函数,因此需要一种机制在测试完成后,再取消注册该回调函数,有两种方式可以此种临时注册的机制:

  • 一种是通过contextmanagerdecorator和disconnect函数一起来实现,如下:
from flask import template_rendered
from contextlib import contextmanager

@contextmanager
def captured_templates(app):
    recorded = []
    def record(sender, template, context, **extra):
        recorded.append((template, context))
    # 当使用with关键字进入with context时,自动注册record函数到template_rendered signal
    template_rendered.connect(record, app)
    try:
        yield recorded
    finally:
        # with context结束时会自动调用disconnect函数来解除注册
        template_rendered.disconnect(record, app)

使用时代码如下:

with captured_templates(app) as templates:
    rv = app.test_client().get('/')
    assert rv.status_code == 200
    assert len(templates) == 1
    template, context = templates[0]
    assert template.name == 'index.html'
    assert len(context['items']) == 10
  • 另外一种方式是使用connect_to函数
from flask import template_rendered

def captured_templates(app, recorded, **extra):
    def record(sender, template, context):
        recorded.append((template, context))
    return template_rendered.connected_to(record, app)

使用时代码如下:

templates = []
with captured_templates(app, templates, **extra):
    ...
    template, context = templates[0]

自定义signals的使用

自定义signal

当我们需要自定义signal时,我们可以直接使用blinker库

  1. 首先定义一个namespace
from blinker import Namespace
my_signals = Namespace()
  1. 使用我们自定义的namespace定义自己的signal
upload_image_finished = my_signals.signal('upload_image_finished')

至此,我们就定义了一个signal,名为upload_image_finished

发射自定义signal

from flask import current_app

def upload_image(image_path, upload_url):
    # upload image code
    ...
    # after upload image
    upload_image_finished.send(current_app._get_current_object())
  • 当在类的method中使用send函数发射signal时,我们可以选择该类的对象作为sender对象,因此直接使用self作为参数,但是当我们不是在类的method当中,或者我们想让应用对象作为sender,那么我们就需使用如上代码所示的current_app._get_current_object()来获取应用对象
  • 使用sender时,第一个参数是sender对象,是必选的。其余实际我们还可以传递更多参数(记得我们的callback函数使用了**extra), 这样的话我们实际就拥有了传递更多数据的能力。

注册回调函数的简化写法

从文章的第一个示例可以看出我们需要通过调用connect函数来对回调函数进行注册, 其实还有一个简化的写法可以把回调函数的定义和注册过程结合在一起,如下:

from flask import template_rendered

@template_rendered.connect_via(app)
def when_template_rendered(sender, template, context, **extra):
    print 'Template %s is rendered with %s' % (template.name, context)

通过connect_via装饰器来简化回调函数定义和注册的过程

Reference

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

推荐阅读更多精彩内容

  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,159评论 22 257
  • 本文首发于Gevin的博客 原文链接:Flask Signals 入门 未经 Gevin 授权,禁止转载 1. 如...
    Gevin阅读 2,106评论 0 12
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 关于Flask登录认证的详细过程请参见拙作<<使用Flask实现用户登陆认证的详细过程>>一文,而本文则偏重于详细...
    geekpy阅读 28,649评论 5 28
  • 最近在学习flask,用到flask-login,发现网上只有0.1版本的中文文档,看了官方已经0.4了,并且添加...
    ZZES_ZCDC阅读 5,934评论 3 24