Inside Flask - signal 信号机制

时间:2022-09-01 21:09:44

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