信号

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)[source]
参数:
  • 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)[source]
参数:
  • 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): ...

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

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

防止重复信号

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

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

from django.core.signals import request_finished

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

定义和发送信号

您的应用程序可以利用信号基础结构并提供其自己的信号。

何时使用自定义信号

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

定义信号

class Signal[source]

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

例如

import django.dispatch

pizza_done = django.dispatch.Signal()

这声明了一个 pizza_done 信号。

发送信号

有两种方法可以在 Django 中同步发送信号。

Signal.send(sender, **kwargs)[source]
Signal.send_robust(sender, **kwargs)[source]

信号也可以异步发送。

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)[source]

要断开信号的接收器连接,请调用 Signal.disconnect()。参数如 Signal.connect() 中所述。如果断开了接收器连接,则该方法返回 True,否则返回 False。当 sender 作为对 <app label>.<model> 的延迟引用传递时,此方法始终返回 None

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

返回顶部