信号

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

例如,第三方应用可以注册以接收设置更改的通知

from django.apps import AppConfig
from django.core.signals import setting_changed


def my_callback(sender, **kwargs):
    print("Setting changed!")


class MyAppConfig(AppConfig):
    ...

    def ready(self):
        setting_changed.connect(my_callback)

Django 的内置信号允许用户代码收到某些操作的通知。

您还可以定义和发送自己的自定义信号。请参阅下面的定义和发送信号

警告

信号看起来是松散耦合的,但它们很快会导致难以理解、调整和调试的代码。

在可能的情况下,您应该选择直接调用处理代码,而不是通过信号分发。

监听信号

要接收信号,请使用Signal.connect()方法注册一个接收器函数。当信号发送时,将调用接收器函数。所有信号的接收器函数将按注册顺序逐个调用。

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);所有信号处理程序都必须采用这些参数。

稍后我们将了解发送者 但现在先了解 **kwargs 参数。所有信号都发送关键字参数,并且可以随时更改这些关键字参数。对于 request_finished,它被记录为不发送任何参数,这意味着我们可能倾向于将信号处理写为 my_callback(sender)

这样做是错误的——事实上,如果你这样做,Django 将会抛出一个错误。这是因为在任何时间点,都可能向信号添加参数,而你的接收器必须能够处理这些新参数。

接收器也可以是异步函数,具有相同的签名,但使用 async def 声明

async def my_callback(sender, **kwargs):
    await asyncio.sleep(5)
    print("Request finished!")

信号可以同步或异步发送,接收器将自动适应正确的调用样式。有关更多信息,请参见 发送信号

Django 5.0 中已更改

添加了对异步接收器的支持。

连接接收器函数

你可以通过两种方式将接收器连接到信号。你可以采取手动连接方式

from django.core.signals import request_finished

request_finished.connect(my_callback)

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

receiver(signal, **kwargs)
参数
  • signal – 要将函数连接到的信号或信号列表。
  • kwargs – 要传递给 函数 的通配符关键字参数。

以下是使用装饰器进行连接的方法

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 子模块,这将隐式连接信号处理程序

from django.apps import AppConfig
from django.core.signals import request_finished


class MyAppConfig(AppConfig):
    ...

    def ready(self):
        # Implicitly connect signal handlers decorated with @receiver.
        from . import signals

        # Explicitly connect a signal handler.
        request_finished.connect(signals.my_callback)

注意

在测试期间,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.asend(sender, **kwargs)
Signal.asend_robust(sender, **kwargs)

若要发送信号,请调用 Signal.send()Signal.send_robust()await Signal.asend()await Signal.asend_robust()。您必须提供 sender 参数(通常为类),并且可以提供任意数量的其他关键字参数。

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

class PizzaStore:
    ...

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

所有四种方法都会返回元组对列表 [(receiver, response), ...],表示已调用的接收器函数列表及其响应值。

send()send_robust() 的不同之处在于如何处理接收器函数引发的异常。 send() 不会捕获接收器引发的任何异常;它只是允许错误传播。因此,在出现错误的情况下,并非所有接收器都会收到信号通知。

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

调用 send_robust() 时,返回的错误的 __traceback__ 属性中存在回溯。

asend() 类似于 send(),但它是一个必须等待的协程

async def asend_pizza(self, toppings, size):
    await pizza_done.asend(sender=self.__class__, toppings=toppings, size=size)
    ...

无论是同步还是异步,接收器都会正确地适应是否使用 send()asend()。通过 asend() 调用时,将使用 sync_to_async() 调用同步接收器。通过 sync() 调用时,将使用 async_to_sync() 调用异步接收器。类似于 中间件的情况,以这种方式调整接收器会产生少量性能开销。请注意,为了减少 send()asend() 调用中同步/异步调用样式切换的次数,在调用接收器之前,会按它们是否是异步对它们进行分组。这意味着在同步接收器之前注册的异步接收器可能会在同步接收器之后执行。此外,异步接收器使用 asyncio.gather() 并发执行。

除了异步请求-响应周期中的信号外,所有内置信号都使用 Signal.send() 发送。

Django 5.0 中已更改

添加了对异步信号的支持。

断开信号

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

要断开接收器与信号的连接,请调用 Signal.disconnect()。参数的说明请参见 Signal.connect()。如果断开了一个接收器,此方法将返回 True,否则返回 False。当 sender 作为 <app label>.<model> 的惰性引用传递时,此方法始终返回 None

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

返回顶部