首页 > 技术文章 > Inside Flask - signal 信号机制

fengyc 2016-09-17 23:25 原文

Inside Flask - signal 信号机制

singal 在平常的 flask web 开发过程中较少接触到,但对于使用 flask 进行框架级别的开发时,则必须了解相关的工作机制。flask 通过 singal 机制,通知上层代码当前 flask 正在进行的处理动作,以便上层代码在 flask 进行处理的前后进行相关的处理(类似于 java 中通过 AOP 拦截操作,在 before action 和 after action 中进行一些处理动作)。

singal 一般只用于通知目的,不应该修改内部数据。它的底层通过 blinker 库实现,如果没有安装这个库(需要额外通过 pip 安装,flask 默认的依赖中不包含 blinker ),那么信号机制将不起作用,此段处理代码为 ::

signals_available = False
try:
    from blinker import Namespace
    signals_available = True
except ImportError:
    class Namespace(object):
        def signal(self, name, doc=None):
            return _FakeSignal(name, doc)

在 import blinker 库失败时,用内部的 _FakeSignal 类取代,它只有一个进行警告的作用,没有实际的处理 ::

class _FakeSignal(object):
    ...
    def __init__(self, name, doc=None):
        self.name = name
        self.__doc__ = doc
    def _fail(self, *args, **kwargs):
        raise RuntimeError('signalling support is unavailable '
                           'because the blinker library is '
                           'not installed.')
    send = lambda *a, **kw: None
    connect = disconnect = has_receivers_for = receivers_for = \
        temporarily_connected_to = connected_to = _fail
    del _fail

此时 signal 的 connect 、 disconnect 、 has_receives_forreceviers_fortemporarily_connected_toconnected_to 等方法,都被替换为 _fail

除了 flask 自身的一些信号,其它插件也可以提供一些信号,像 flask-login 的信号机制

现在深入看看 flask 的信号机制和它所依赖的 blinker 库的信号机制实现。

blinker 库通过信号的方式,解除组件之间的耦合。blinker 非常小(只包括 3 个源文件),但提供了一个面向对象的消息机制,具体可见blinker 文档

blinker 的主要内容在 base.py 文件。该文件包含了 Signal 、 NamedSignal 、 Namespace 、 WeekNamespace 等几个主要的类。

Signal 类是最关键的类,表示一个特定的信号,提供了对信号的基本操作方法: connect 、 disconnect 、 send 等。它包含3个主要的概念: sender 、 receiver 、 signal 。sender 是指信号的发送者对象,receiver 是信号的接收者(一个 callable 的对象),receiver 通过 hash 方法计算一个 id 作为其在 Signal 中的 key,signal 则是当前的信号对象。它的初始化过程如下 ::

def __init__(self, doc=None):
    ...
    if doc:
        self.__doc__ = doc
    ...
    self.receivers = {}
    self._by_receiver = defaultdict(set)
    self._by_sender = defaultdict(set)
    self._weak_senders = {}

receivers 是一个 receiver 的 id 和引用(原始对象引用或弱引用 weakref)的字典。_by_receiver_by_sender 用于辅助查找,一个是通过 receiver id 查找对应的 sender id 集合,一个是通过 sender id 查找对应的 receiver id 集合。

订阅信号时,使用 connect 方法(或 connect_via 装饰器),处理过程如下 ::

def connect(self, receiver, sender=ANY, weak=True):
    ...
    receiver_id = hashable_identity(receiver)
    if weak:
        receiver_ref = reference(receiver, self._cleanup_receiver)
        receiver_ref.receiver_id = receiver_id
    else:
        receiver_ref = receiver
    if sender is ANY:
        sender_id = ANY_ID
    else:
        sender_id = hashable_identity(sender)

    self.receivers.setdefault(receiver_id, receiver_ref)
    self._by_sender[sender_id].add(receiver_id)
    self._by_receiver[receiver_id].add(sender_id)
    ...

首先,通过 hash 计算一个 receiver 的 id 作为 key ,并计算 sender 的 id 。然后,将 receiver id 和其引用保存到 receivers 字典,sender 与 receiver 的对应关系分别保存到 _by_sender_by_receiver 中,供后续查找时使用。添加成功时,会广播此次的 connection (每个 signal 有一个 receiver_connected 信号 ,全局还有一个) ::

if ('receiver_connected' in self.__dict__ and
    self.receiver_connected.receivers):
    try:
        self.receiver_connected.send(self,
                                     receiver=receiver,
                                     sender=sender,
                                     weak=weak)
    except:
        self.disconnect(receiver, sender)
        raise
if receiver_connected.receivers and self is not receiver_connected:
    try:
        receiver_connected.send(self,
                                receiver_arg=receiver,
                                sender_arg=sender,
                                weak_arg=weak)
    except:
        self.disconnect(receiver, sender)
        raise

成功使用 connect 订阅信号后,可以进行信号的发送,代码如下 ::

def send(self, *sender, **kwargs):
    ...
    # Using '*sender' rather than 'sender=None' allows 'sender' to be
    # used as a keyword argument- i.e. it's an invisible name in the
    # function signature.
    if len(sender) == 0:
        sender = None
    elif len(sender) > 1:
        raise TypeError('send() accepts only one positional argument, '
                        '%s given' % len(sender))
    else:
        sender = sender[0]
    if not self.receivers:
        return []
    else:
        return [(receiver, receiver(sender, **kwargs))
                for receiver in self.receivers_for(sender)]

在 send 的时候,Signal 查找 sender 对应的 receiver 列表,然后逐个调用。receivers_for 函数查找 sender 对应的 receiver 列表。

最后,如果要取消订阅,就用 disconnect 。

NamedSignal 是在 Signal 的基础上,加上一个 name 变量,作为命名的信号。

Namespace 是一个管理信号的字典,它提供 signal 工厂方法,并自动创建相应名字的 NamedSignal ,如下 ::

def signal(self, name, doc=None):
    ...
    try:
        return self[name]
    except KeyError:
        return self.setdefault(name, NamedSignal(name, doc))

WeekNamespace 中是 Namespace 的弱引用改进版本,继承自 WeakValueDictionary 。

在 flask 中,flask 定义了自己的 Namespace 用于隔离,然后建立一系列内置的信号。在 flask/signals.py 中 ::

_signals = Namespace()
...
template_rendered = _signals.signal('template-rendered')
before_render_template = _signals.signal('before-render-template')
request_started = _signals.signal('request-started')
request_finished = _signals.signal('request-finished')
request_tearing_down = _signals.signal('request-tearing-down')
got_request_exception = _signals.signal('got-request-exception')
appcontext_tearing_down = _signals.signal('appcontext-tearing-down')
appcontext_pushed = _signals.signal('appcontext-pushed')
appcontext_popped = _signals.signal('appcontext-popped')
message_flashed = _signals.signal('message-flashed')

每个信号的具体使用见 http://docs.jinkan.org/docs/flask/signals.html

推荐阅读