信号

Django有一个“信号调度器(signal dispatcher)”,用来帮助解耦的应用获知框架内任何其他地方发生了操作。简单地说,信号允许某些 发送器 去通知一组 接收器 某些操作发生了。当许多代码段都可能对同一事件感兴趣时,信号特别有用。

For example, a third-party app can register to be notified of settings changes:

  1. from django.apps import AppConfig
  2. from django.core.signals import setting_changed
  3. def my_callback(sender, **kwargs):
  4. print("Setting changed!")
  5. class MyAppConfig(AppConfig):
  6. ...
  7. def ready(self):
  8. setting_changed.connect(my_callback)

Django’s built-in signals let user code get notified of certain actions.

You can also define and send your own custom signals. See 定义和发送信号 below.

警告

Signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug.

Where possible you should opt for directly calling the handling code, rather than dispatching via a signal.

监听信号

要接收信号,使用 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 函数或方法:

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

注意,该函数接收一个 sender 参数以及关键字参数 (**kwargs);所有信号处理程序都必须接受这些参数。

We’ll look at senders a bit later, but right now look at the **kwargs argument. All signals send keyword arguments, and may change those keyword arguments at any time. In the case of request_finished, it’s documented as sending no arguments, which means we might be tempted to write our signal handling as my_callback(sender).

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

Receivers may also be asynchronous functions, with the same signature but declared using async def:

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

Signals can be sent either synchronously or asynchronously, and receivers will automatically be adapted to the correct call-style. See sending signals for more information.

Changed in Django 5.0:

Support for asynchronous receivers was added.

连接接收器函数

有两种方法可以将接收器连接到信号。你可以选择手动连接线路:

  1. from django.core.signals import request_finished
  2. request_finished.connect(my_callback)

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

receiver(signal, **kwargs)

参数:
  • signal — 一个用于连接函数的信号或包含多个信号的列表。
  • kwargs — Wildcard keyword arguments to pass to a function.

以下是你如何使用装饰器连接:

  1. from django.core.signals import request_finished
  2. from django.dispatch import receiver
  3. @receiver(request_finished)
  4. def my_callback(sender, **kwargs):
  5. print("Request finished!")

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

我的代码该放在哪?

严格来说,信号处理和注册的代码可以放在任何你喜欢的地方,但是推荐避免放在应用程序的根目录和 models 模块内以尽量减少导入代码的副作用。

In practice, signal handlers are usually defined in a signals submodule of the application they relate to. Signal receivers are connected in the ready() method of your application configuration class. If you’re using the receiver() decorator, import the signals submodule inside ready(), this will implicitly connect signal handlers:

  1. from django.apps import AppConfig
  2. from django.core.signals import request_finished
  3. class MyAppConfig(AppConfig):
  4. ...
  5. def ready(self):
  6. # Implicitly connect signal handlers decorated with @receiver.
  7. from . import signals
  8. # Explicitly connect a signal handler.
  9. request_finished.connect(signals.my_callback)

备注

ready() 方法在测试过程中可能会多次执行,因此你可能需要 防止重复信号,尤其是当您计划在测试中发送信号时。

连接到特定发送器发送的信号

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

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

  1. from django.db.models.signals import pre_save
  2. from django.dispatch import receiver
  3. from myapp.models import MyModel
  4. @receiver(pre_save, sender=MyModel)
  5. def my_handler(sender, **kwargs):
  6. ...

my_handler 函数将仅在 MyModel 实例保存后被调用。

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

防止重复信号

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

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

  1. from django.core.signals import request_finished
  2. request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")

定义和发送信号

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

何时使用自定义信号

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

定义信号

class Signal

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

例如:

  1. import django.dispatch
  2. pizza_done = django.dispatch.Signal()

这声明了一个 pizza_done 信号。

发送信号

There are two ways to send signals synchronously in Django.

Signal.send(sender, **kwargs)

Signal.send_robust(sender, **kwargs)

Signals may also be sent asynchronously.

Signal.asend(sender, **kwargs)

Signal.asend_robust(sender, **kwargs)

To send a signal, call either Signal.send(), Signal.send_robust(), await Signal.asend(), or await Signal.asend_robust(). You must provide the sender argument (which is a class most of the time) and may provide as many other keyword arguments as you like.

例如,发送 pizza_done 信号可能看起来如下:

  1. class PizzaStore:
  2. ...
  3. def send_pizza(self, toppings, size):
  4. pizza_done.send(sender=self.__class__, toppings=toppings, size=size)
  5. ...

All four methods return a list of tuple pairs [(receiver, response), ...], representing the list of called receiver functions and their response values.

send()send_robust() 在处理接收器函数所引发异常的方式上有所不同。 send() 捕获接收器引起的任何异常;它只是允许错误传播。因此,并非所有的接收器都会在出现错误时被通知信号。

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

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

asend() is similar as send(), but it is coroutine that must be awaited:

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

Whether synchronous or asynchronous, receivers will be correctly adapted to whether send() or asend() is used. Synchronous receivers will be called using sync_to_async() when invoked via asend(). Asynchronous receivers will be called using async_to_sync() when invoked via sync(). Similar to the case for middleware, there is a small performance cost to adapting receivers in this way. Note that in order to reduce the number of sync/async calling-style switches within a send() or asend() call, the receivers are grouped by whether or not they are async before being called. This means that an asynchronous receiver registered before a synchronous receiver may be executed after the synchronous receiver. In addition, async receivers are executed concurrently using asyncio.gather().

All built-in signals, except those in the async request-response cycle, are dispatched using Signal.send().

Changed in Django 5.0:

Support for asynchronous signals was added.

断开信号

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

To disconnect a receiver from a signal, call Signal.disconnect(). The arguments are as described in Signal.connect(). The method returns True if a receiver was disconnected and False if not. When sender is passed as a lazy reference to <app label>.<model>, this method always returns None.

receiver 参数表明要断开的接收器。它可以是 None 如果 dispatch_uid 已经被用来标识接收器。