信号 — Django 文档

来自菜鸟教程
Django/docs/3.1.x/topics/signals
跳转至:导航、​搜索

信号

Django 包含一个“信号调度器”,它有助于在框架中的其他地方发生动作时让解耦的应用程序得到通知。 简而言之,信号允许某些 发送者 通知一组 接收者 某些动作已经发生。 当许多代码段可能对同一事件感兴趣时,它们特别有用。

Django 提供了一组 内置信号 ,可以让 Django 本身通知用户代码某些操作。 其中包括一些有用的通知:

有关完整列表以及每个信号的完整说明,请参阅 内置信号文档

您也可以定义和发送您自己的自定义信号; 见下文。

收听信号

要接收信号,请使用 Signal.connect() 方法注册 receiver 函数。 接收器函数在发送信号时被调用。 一次调用一个信号的所有接收器函数,按照它们注册的顺序。

Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None)
;; 参数
;;* receiver – 将连接到该信号的回调函数。 有关详细信息,请参阅 接收器函数
  • sender – 指定一个特定的发送者来接收信号。 有关详细信息,请参阅 连接到特定发送者发送的信号
  • weak – 默认情况下,Django 将信号处理程序存储为弱引用。 因此,如果您的接收器是一个本地函数,它可能会被垃圾收集。 为防止这种情况,请在调用信号的 connect() 方法时传递 weak=False
  • dispatch_uid – 在可能发送重复信号的情况下,信号接收器的唯一标识符。 有关详细信息,请参阅 防止重复信号

让我们通过注册一个在每个 HTTP 请求完成后调用的信号来看看这是如何工作的。 我们将连接到 request_finished 信号。

接收器功能

首先,我们需要定义一个接收器函数。 接收器可以是任何 Python 函数或方法:

def my_callback(sender, **kwargs):
    print("Request finished!")

请注意,该函数采用 sender 参数以及通配符关键字参数 (**kwargs); 所有信号处理程序都必须采用这些参数。

我们稍后会查看发送方 [[#connecting-to-signals-sent-by-specific-senders|]] ,但现在查看 **kwargs 参数。 所有信号都发送关键字参数,并且可以随时更改这些关键字参数。 在 request_finished 的情况下,它被记录为不发送参数,这意味着我们可能会尝试将我们的信号处理写为 my_callback(sender)

这是错误的——事实上,如果你这样做,Django 会抛出一个错误。 这是因为在任何时候参数都可能被添加到信号中,并且您的接收器必须能够处理这些新参数。


连接接收器功能

有两种方法可以将接收器连接到信号。 您可以采用手动连接路线:

from django.core.signals import request_finished

request_finished.connect(my_callback)

或者,您可以使用 receiver() 装饰器:

receiver(signal)
;; 参数
signal – 连接函数的信号或信号列表。

以下是您与装饰器连接的方式:

from django.core.signals import request_finished
from django.dispatch import receiver

@receiver(request_finished)
def my_callback(sender, **kwargs):
    print("Request finished!")

现在,每次请求完成时都会调用我们的 my_callback 函数。

这段代码应该放在哪里?

严格来说,信号处理和注册代码可以放在你喜欢的任何地方,尽管建议避免应用程序的根模块及其 models 模块以尽量减少导入代码的副作用。

在实践中,信号处理程序通常定义在与它们相关的应用程序的 signals 子模块中。 信号接收器在应用程序配置类的 ready() 方法中连接。 如果您使用 receiver() 装饰器,请在 ready() 中导入 signals 子模块。


笔记

ready() 方法在测试期间可能会执行多次,因此您可能需要 保护您的信号免于重复 ,特别是如果您计划在测试中发送它们。


连接到特定发件人发送的信号

有些信号会被多次发送,但您只会对接收这些信号的某个子集感兴趣。 例如,考虑在保存模型之前发送的 django.db.models.signals.pre_save 信号。 大多数情况下,您不需要知道何时保存 任何 模型 - 只需保存一个 特定 模型。

在这些情况下,您可以注册接收仅由特定发件人发送的信号。 在 django.db.models.signals.pre_save 的情况下,发送者将是正在保存的模型类,因此您可以表明您只想要某个模型发送的信号:

from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel


@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
    ...

my_handler 函数只有在保存了 MyModel 的实例时才会被调用。

不同的信号使用不同的对象作为发送者; 您需要查阅 内置信号文档 以了解每个特定信号的详细信息。


防止重复信号

在某些情况下,将接收器连接到信号的代码可能会运行多次。 这可能会导致您的接收器函数被多次注册,从而为一个信号事件调用多次。 例如,ready() 方法在测试期间可能会执行多次。 更一般地,这发生在您的项目导入定义信号的模块的任何地方,因为信号注册运行的次数与导入的次数一样多。

如果此行为有问题(例如在保存模型时使用信号发送电子邮件时),请将唯一标识符作为 dispatch_uid 参数传递以标识您的接收器函数。 这个标识符通常是一个字符串,尽管任何可散列的对象都足够了。 最终结果是您的接收器函数将只针对每个唯一的 dispatch_uid 值与信号绑定一次:

from django.core.signals import request_finished

request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")

定义和发送信号

您的应用程序可以利用信号基础设施并提供自己的信号。

何时使用自定义信号

信号是隐式函数调用,这使得调试更加困难。 如果自定义信号的发送者和接收者都在您的项目中,则最好使用显式函数调用。


定义信号

class Signal

所有信号都是 django.dispatch.Signal 实例。

例如:

import django.dispatch

pizza_done = django.dispatch.Signal()

这声明了一个 pizza_done 信号。


发送信号

在 Django 中有两种发送信号的方法。

Signal.send(sender, **kwargs)
Signal.send_robust(sender, **kwargs)

要发送信号,请调用 Signal.send()(所有内置信号都使用它)或 Signal.send_robust()。 您必须提供 sender 参数(大多数情况下这是一个类),并且可以提供任意数量的其他关键字参数。

例如,以下是发送 pizza_done 信号的方式:

class PizzaStore:
    ...

    def send_pizza(self, toppings, size):
        pizza_done.send(sender=self.__class__, toppings=toppings, size=size)
        ...

send()send_robust() 都返回一个元组对列表 [(receiver, response), ... ],表示被调用的接收器函数及其响应值的列表。

send()send_robust() 的不同之处在于如何处理接收器函数引发的异常。 send() 是否 not 捕获接收者引发的任何异常; 它只是允许错误传播。 因此,并非所有接收器都可以在遇到错误时收到信号通知。

send_robust() 捕获源自 Python 的 Exception 类的所有错误,并确保所有接收器都收到信号通知。 如果发生错误,则错误实例在引发错误的接收者的元组对中返回。

回溯出现在调用 send_robust() 时返回的错误的 __traceback__ 属性上。


断开信号

Signal.disconnect(receiver=None, sender=None, dispatch_uid=None)

要断开接收器与信号的连接,请调用 Signal.disconnect()。 参数如 Signal.connect() 中所述。 如果接收器断开连接,该方法返回 True,否则返回 False

receiver 参数表示已注册的接收器要断开连接。 如果使用dispatch_uid来识别接收者,则可能是None