Сигналы

Django включает в себя «диспетчер сигналов», который позволяет разделенным приложениям получать уведомления о действиях, происходящих в других частях фреймворка. В двух словах, сигналы позволяют определенным отправителям уведомлять набор получателей о том, что произошло какое-то действие. Они особенно полезны, когда многие части кода могут быть заинтересованы в одних и тех же событиях.

Django предоставляет set of built-in signals, который позволяет пользовательскому коду получать уведомления от самого Django об определенных действиях. Среди них есть несколько полезных уведомлений:

Полный список и полное объяснение каждого сигнала см. в built-in signal documentation.

Вы также можете define and send your own custom 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 выдаст ошибку, если вы так сделаете. Это потому, что в любой момент в сигнал могут быть добавлены аргументы, и ваш приемник должен быть в состоянии обработать эти новые аргументы.

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

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

from django.core.signals import request_finished

request_finished.connect(my_callback)

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

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

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

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().

Примечание

Метод 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.send() (все встроенные сигналы используют это), либо Signal.send_robust(). Вы должны предоставить аргумент sender (который в большинстве случаев является классом) и можете предоставить любое количество других аргументов с ключевыми словами.

Например, вот как может выглядеть отправка нашего сигнала pizza_done:

class PizzaStore:
    ...

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

И send(), и send_robust() возвращают список кортежных пар [(receiver, response), ... ], представляющих список вызванных функций-приемников и их ответные значения.

send() отличается от send_robust() тем, как обрабатываются исключения, поднятые функциями-приемниками. send() не перехватывает исключения, вызванные приемниками; он просто позволяет ошибкам распространяться. Таким образом, не все приемники могут быть уведомлены о сигнале при возникновении ошибки.

send_robust() ловит все ошибки, производные от класса Python Exception, и обеспечивает уведомление всех приемников о сигнале. Если произошла ошибка, экземпляр ошибки возвращается в кортеже для приемника, который вызвал ошибку.

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

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

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

Чтобы отключить приемник от сигнала, вызовите Signal.disconnect(). Аргументы описаны в Signal.connect(). Метод возвращает True, если приемник был отключен, и False, если нет.

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

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