Сигналы
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 хранит обработчики сигналов в виде слабых ссылок. Поэтому локальная функция-получатель может быть удалена сборщиком мусора. Чтобы этого не произошло, передайте
weak=Falseпри вызове методаconnect()сигнала. - 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!")
Сигналы можно отправлять как синхронно, так и асинхронно, а получатели автоматически адаптируются к нужному стилю вызова. Дополнительные сведения см. в разделе Отправка сигналов.
Подключение функций-получателей
Есть два способа подключить получателя к сигналу. Можно подключиться вручную:
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(), импортируйте подмодуль signals внутри ready() — это неявно подключит обработчики сигналов:
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() перехватывает все ошибки, производные от класса Exception в Python, и гарантирует, что все получатели будут уведомлены о сигнале. Если произошла ошибка, экземпляр ошибки возвращается в паре кортежа для получателя, вызвавшего эту ошибку.
Трассировки стека доступны в атрибуте __traceback__ ошибок, возвращаемых при вызове send_robust().
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(). Асинхронные получатели при вызове через send() вызываются с помощью async_to_sync(). Как и в случае с промежуточным ПО, такая адаптация получателей сопряжена с небольшими затратами производительности. Обратите внимание: чтобы уменьшить количество переключений между синхронным и асинхронным стилями вызова внутри вызова send() или asend(), получатели перед вызовом группируются в зависимости от того, являются ли они асинхронными. Это означает, что асинхронный получатель, зарегистрированный перед синхронным, может выполниться после синхронного. Кроме того, асинхронные получатели выполняются конкурентно с помощью asyncio.gather().
Все встроенные сигналы, кроме сигналов в асинхронном цикле запрос-ответ, отправляются с помощью Signal.send().
Отключение сигналов
-
Signal.disconnect(receiver=None, sender=None, dispatch_uid=None)[исходный код]
Чтобы отключить получателя от сигнала, вызовите Signal.disconnect(). Аргументы описаны в разделе Signal.connect(). Метод возвращает True, если получатель был отключён, и False, если нет. Если sender передан в качестве отложенной ссылки на <app label>.<model>, этот метод всегда возвращает None.
Аргумент receiver указывает зарегистрированного получателя, которого нужно отключить. Он может быть None, если для идентификации получателя используется dispatch_uid.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/topics/signals/