Привет, Хабр!
Сегодня я расскажу, как использовать Django Signals, чтобы приложение работало как часы. Signals — это встроенный механизм в Django, который позволяет разным частям приложения «общаться» друг с другом через события.
Зачем они нужны:
Нужно реагировать на определённые действия в приложении (например, сохранение объекта или завершение запроса).
Нужно изолировать логику обработки событий, чтобы основной код оставался чистым.
Не хотите вмешиваться в сторонние приложения, но хотите знать, когда там что‑то происходит.
Как это работает:
Отправитель (Sender): генерирует событие.
Сигнал (Signal): уведомляет зарегистрированные обработчики.
Обработчики (Receivers): реагируют на сигнал, выполняя нужную логику.
Когда использовать Signals?
Когда они полезны:
Разделение логики. Уведомления, фоновые задачи, простая обработка событий — всё это отлично ложится на сигналы.
Модульность. Можно добавлять обработчики без изменения основного кода.
Многоуровневая обработка. Например, сохранение объекта запускает несколько независимых действий.
Когда лучше не использовать:
Прямая зависимость. Если обработчик используется только в одном месте, вызовите функцию напрямую.
Сложная бизнес‑логика. Переносить её в сигналы — плохая идея. Вы создадите код, который сложно понять и отладить.
Основы работы с Signals
Django имеет массу встроенных сигналов:
pre_save
иpost_save
— до и после сохранения объекта.pre_delete
иpost_delete
— до и после удаления объекта.request_started
иrequest_finished
— в начале и конце запроса.user_logged_in
,user_logged_out
— пользователь вошёл/вышел.
Как подключать обработчики:
-
Через метод
connect
:from django.db.models.signals import post_save from myapp.models import MyModel def my_handler(sender, **kwargs): print("Объект сохранён!") post_save.connect(my_handler, sender=MyModel)
-
Через декоратор
@receiver
:from django.db.models.signals import post_save from django.dispatch import receiver from myapp.models import MyModel @receiver(post_save, sender=MyModel) def my_handler(sender, **kwargs): print("Объект сохранён!")
Пример применения на магазине котиков
Итак, у нас есть магазин котиков. Что мы хотим:
Уведомлять пользователей о регистрации.
Обрабатывать заказы.
Уведомлять о поступлении новых котиков.
Проверять зависимые данные при удалении котика.
Уведомление о регистрации
Когда пользователь регистрируется, отправляем ему приветственное письмо.
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.mail import send_mail
from myapp.models import CustomUser
@receiver(post_save, sender=CustomUser)
def send_welcome_email(sender, instance, created, **kwargs):
if created:
send_mail(
subject='Добро пожаловать в Магазин Котиков!',
message=f'Привет, {instance.username}! Рады видеть тебя среди любителей котиков!',
from_email='no-reply@catshop.com',
recipient_list=[instance.email],
)
Сигнал post_save
ловит событие создания нового пользователя. Проверяем, что объект новый, и отправляем письмо.
Обработка заказа
Когда пользователь покупает котика, нужно:
Уменьшить его количество на складе.
Отправить уведомление о покупке.
Логировать заказ.
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Order, Cat
from django.core.mail import send_mail
@receiver(post_save, sender=Order)
def process_order(sender, instance, created, **kwargs):
if created:
cat = instance.cat
if cat.stock < instance.quantity:
raise ValueError("Недостаточно котиков на складе!")
cat.stock -= instance.quantity
cat.save()
send_mail(
subject='Ваш заказ принят!',
message=f'Спасибо за покупку котика {cat.name}! Скоро он будет у вас.',
from_email='no-reply@catshop.com',
recipient_list=[instance.user.email],
)
print(f"Пользователь {instance.user.username} купил {instance.quantity} котика(ов) {cat.name}.")
Уведомление о новых котиках
Когда поступают новые котики, отправляем уведомление подписчикам.
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Cat, Subscription
from django.core.mail import send_mail
@receiver(post_save, sender=Cat)
def notify_about_new_cats(sender, instance, created, **kwargs):
if created and instance.stock > 0:
subscribers = Subscription.objects.filter(is_active=True).select_related('user')
emails = [sub.user.email for sub in subscribers]
send_mail(
subject='Новые котики на складе!',
message=f'У нас появились новые котики: {instance.name}. Цена: {instance.price} руб. Успейте купить!',
from_email='no-reply@catshop.com',
recipient_list=emails,
)
Удаление котика с проверкой зависимостей
Проверяем, нет ли активных заказов на удаляемого котика.
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from myapp.models import Cat, Order
from django.core.mail import send_mail
@receiver(pre_delete, sender=Cat)
def check_before_delete(sender, instance, **kwargs):
active_orders = Order.objects.filter(cat=instance)
if active_orders.exists():
raise ValueError(f"Котик {instance.name} фигурирует в заказах. Удаление невозможно.")
send_mail(
subject='Котик удалён из магазина',
message=f'Котик {instance.name} был удалён. Проверьте связанные заказы.',
from_email='no-reply@catshop.com',
recipient_list=['manager@catshop.com'],
)
Прочие фичи Signals
Доступна асинхронная обработка заказа для долгих задач:
@receiver(post_save, sender=Order)
async def async_process_order(sender, instance, **kwargs):
await asyncio.sleep(1) # Эмуляция долгой операции
print(f"Асинхронная обработка заказа на {instance.cat.name}.")
Также можно создавать кастомные сигналы. Создадим свой сигнал «котик продан»:
from django.dispatch import Signal
cat_sold = Signal()
@receiver(cat_sold)
def notify_about_sale(sender, cat, user, **kwargs):
print(f"Котик {cat.name} был продан пользователю {user.username}.")
Возможные ошибки и как их избежать
Дублирование обработчиков: используйте
dispatch_uid
.Пропущенные аргументы: всегда добавляйте
sender
и**kwargs
.Избыточность сигналов: используйте их только для изоляции логики.
Пропуск
sender
: всегда указывайте конкретный отправитель.Неправильное место хранения: храните сигналы в
signals.py
и подключайте черезapps.py
.
А как вы используете Signals? Делитесь своими кейсами в комментариях! ?
Python-разработчикам, заинтересованным в профессиональном развитии, рекомендую обратить внимание на открытые уроки:
13 января. Docker для Python-разработчика: разберём лучшие практики написания Dockerfile, изучим принципы работы с Docker и обсудим тонкости контейнеризации приложений. Особое внимание уделим специфике контейнеризации Python-приложений. Записаться
23 января. Асинхронное взаимодействие в Python на примере RabbitMQ: рассмотрим пример построения архитектуры приложения, разберемся в преимуществах и недостатках такого подхода. Записаться
GospodinKolhoznik
Безотносительно Django, как только приходится работать с каким либо фрэймворком, работающим через signals-ы, первым делом пишу API-обёртку, позволяющую избавиться от них.