Сигналы

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 built-in signals позволяет пользовательскому коду получать уведомления об определенных действиях.

Вы также можете определять и посылать собственные пользовательские сигналы. См. Определение и отправка сигналов ниже.

Предупреждение

Сигналы создают видимость свободной связи, но они могут быстро привести к коду, который трудно понять, настроить и отладить.

По возможности следует выбирать прямой вызов кода обработки, а не диспетчеризацию через сигнал.

Прослушивание сигналов

Чтобы принять сигнал, зарегистрируйте функцию приемник с помощью метода 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); все обработчики сигналов должны принимать эти аргументы.

Мы рассмотрим отправители a bit later, но сейчас обратите внимание на аргумент **kwargs. Все сигналы посылают аргументы с ключевыми словами и могут менять эти аргументы в любое время. В случае с request_finished он документирован как не посылающий никаких аргументов, что означает, что у нас может возникнуть соблазн написать нашу обработку сигнала как my_callback(sender).

Это было бы неправильно - на самом деле, Django выдаст ошибку, если вы так сделаете. Это потому, что в любой момент в сигнал могут быть добавлены аргументы, и ваш приемник должен быть в состоянии обработать эти новые аргументы.

Приемники также могут быть асинхронными функциями, с той же сигнатурой, но объявленными с использованием async def:

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

Сигналы можно посылать как синхронно, так и асинхронно, а приемники автоматически адаптируются к нужному стилю вызова. Дополнительную информацию см. в разделе sending signals.

Changed in Django 5.0:

Добавлена поддержка асинхронных приемников.

Подключение функций приемника

Существует два способа подключения приемника к сигналу. Можно воспользоваться ручным способом подключения:

from django.core.signals import request_finished

request_finished.connect(my_callback)

В качестве альтернативы можно использовать декоратор receiver():

receiver(signal, **kwargs)[исходный код]
Параметры:
  • signal – Сигнал или список сигналов, к которым подключается функция.
  • kwargs – Ключевые аргументы с подстановочными знаками для передачи в function.

Вот как вы связываетесь с декоратором:

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() вашего приложения configuration class. Если вы используете декоратор 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() может быть выполнен более одного раза во время тестирования, поэтому вы можете захотеть guard your signals from duplication, особенно если вы планируете отправлять их внутри тестов.

Подключение к сигналам, посылаемым определенными отправителями

Некоторые сигналы посылаются много раз, но вас будет интересовать получение только определенного подмножества таких сигналов. Например, рассмотрим сигнал 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.

Различные сигналы используют различные объекты в качестве отправителей; вам нужно обратиться к built-in signal documentation для получения подробной информации о каждом конкретном сигнале.

Предотвращение дублирования сигналов

В некоторых обстоятельствах код, соединяющий приемники с сигналами, может выполняться несколько раз. Это может привести к тому, что ваша функция приемника будет зарегистрирована более одного раза и, соответственно, столько же раз вызвана для события сигнала. Например, метод 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, и обеспечивает уведомление всех приемников о сигнале. Если произошла ошибка, экземпляр ошибки возвращается в кортеже для приемника, который вызвал ошибку.

Трассировки присутствуют на атрибуте __traceback__ ошибок, возвращаемых при вызове send_robust().

asend() аналогичен send(), но ожидать нужно именно coroutine:

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

Синхронные или асинхронные приемники будут правильно адаптированы к тому, используется ли send() или asend(). Синхронные приемники будут вызываться с помощью sync_to_async() при вызове через asend(). Асинхронные приемники будут вызываться с помощью async_to_sync() при вызове через sync(). Как и в случае с case for middleware, адаптация приемников таким образом связана с небольшими затратами на производительность. Обратите внимание, что для уменьшения количества переключений между стилями синхронного/асинхронного вызова в рамках вызова send() или asend() приемники группируются по тому, являются ли они асинхронными до вызова. Это означает, что асинхронный приемник, зарегистрированный до синхронного приемника, может быть выполнен после синхронного приемника. Кроме того, асинхронные приемники выполняются параллельно с помощью asyncio.gather().

Все встроенные сигналы, кроме тех, которые находятся в цикле асинхронного запроса-ответа, отправляются с помощью Signal.send().

Changed in Django 5.0:

Добавлена поддержка асинхронных сигналов.

Отключение сигналов

Signal.disconnect(receiver=None, sender=None, dispatch_uid=None)[исходный код]

Чтобы отключить приемник от сигнала, вызовите команду Signal.disconnect(). Аргументы описаны в Signal.connect(). Метод возвращает True, если приемник был отключен, и False, если нет. Когда sender передается как ленивая ссылка на <app label>.<model>, этот метод всегда возвращает None.

Аргумент receiver указывает на зарегистрированный приемник для отключения. Он может быть None, если dispatch_uid используется для идентификации приемника.

Вернуться на верх