Александр Кошелев
Александр Кошелев Python-разработчик

А вы поймали новые сигналы?

Вчера джанга сделал ещё один существенный шаг на пути к долгожданному релизу 1.0. На этот раз посчастливилось обновиться инфраструктуре сигналов. Основное и главное отличие новой подсистемы сигналов - это почти двукратное увеличение производительности.

Исторически сигналы в джанге были сделаны на базе пакета pyDispatcher. который почти без изменений был скопирован в django.dispatch. Со временем выяснилось, что он избыточен и медленен.

Сигналы практически везде пронизывают код джанги. Повесить обработчик можно и на изменение объекта, и на начало и конец обработчик запроса и рендеринг шаблона и много чего ещё. Причем они не только предоставляют некий интерфейс взаимодействия с внешним миром, но и активно применяются внутри самой джанги. Например, удаление загруженных файлов вслед за объектами и закрытие соединения с базой при завершении обработки запроса(!).

Понятно, что производительность всей подсистемы сигналов очень важна и напрямую влияет на скорость отклика приложения. Хотя многие разработчики и не особо их используют в своём коде, а некоторые даже говорят, что они не нужны, всё равно они играют значимую роль в общей производительности. А если ещё вы их активно применяете, как это делаю я для умного кеширования, то вопрос улучшения работы сигналов касается нас очень сильно.

Возможности погонять тесты у меня нет, так что поверим разработчикам на слово. Лучше копнем поглубже в суть.

Конечно, такие большие перемены сказались и на внешнем интерфейсе подсистемы сигналов, поэтому данные изменения обратно не совместимы.

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

Первым делом объявим сигнал:

from django.dispatch import Signal

dinner_is_served = Signal()

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

Как мы знаем, сигналы обычно обозначают наступление какого-либо события. События происходят в каком-то окружении, а следовательно вместе с сигналами распространяется некая мета информация, описывающая как раз контекст его возникновения. Теперь у разработчика есть явная возможность помимо стандартного sender описать ещё дополнительные параметры, с которыми сигнал должен посылаться. Выглядит это так:

dinner_is_served = Signal(providing_args=["courses", "dessert"])

Т.е. по логике при посылке данного сигналы должна быть указано подмножество этих аргументов. Но это нигде не проверяется и получается что это всего лишь такой вариант самодокументирования:) Допилят позже надо полагать.

Ну всё, сигнал объявили и надо его послать всем желающим:

dinner_is_served.send(sender="Barrymore", courses=["soup", "oatmeal porridge"], dessert="pudding")

Добавить то желающих тоже просто:

def sherlock_holmes(sender, courses, dessert, **kwargs):
   pass

dinner_is_served.connect(sherlock_holmes)

Как легко заметить, все операции по присоединению хендлеров и посылки происходят с самим объектом событий. Но для некоторой обратной совместимости, сохранились и старые методы через диспетчер dispatcher.send и dispatcher.connect, но их уже не рекомендуется использовать.

Итак, подведем итог. Для того чтобы максимально просто и безболезненно заставить работать старый код после очередного svn up джанги, надо явным образом создавать объект сигнала. Всё остальное должно работать. Если конечно вы не использовали какие-то более изощренные способы работы с сигналами.

Избавиться совсем от наследия pyDispatcher не получилось. Был оставлен модуль saferef.py в котором реализована “слабая ссылка”(weak reference) и по умолчанию обработчики, присоединяемые к сигналу, таковыми являются. Но это поведение можно изменить, добавив при присоединении хендлера параметр weak=False.

Кстати, косвенно с этим связанна ещё одна интересная особенность подсистемы сигналов.

Представим, что у нас есть код:

def some_func(param):
   def handler(*args, **kwargs):
      pass

   some_signal.connect(handler)

И потом вы эту some_func зовете несколько раз. И когда сигнал сработает, что должно произойти? Правильно. Ничего. Хендлер не сработает потому что из-за “слабой” ссылки GC его убьет, а то что мы его зарегистрировали его не волнует. Поэтому добавляем weak=False:

some_signal.connect(handler, weak=False)

А что теперь будет? Ага, обработчик сработает несколько раз, точнее столько раз сколько мы вызвали функцию some_func в рамках жизни данного процесса. Для того чтобы избежать повторного добавления обработчика, есть очень полезный параметр dispatch_uid, который, если присутствует, должен быть уникальным идентификатором данного хендлера - строка или что угодно hashable. Вот так например:

some_signal.connect(handler, weak=False, dispatch_uid="my_useful_unique_handler")

Занятно то, что такую же проблему я решал когда делал инвалидацию кеша сигналами. И тоже “подписывал” хендлер уникальным идентификатором и на основе него не давал регистрироваться дубликатам. Теперь всё уже сделано за меня, что очень радует.

Шаг очередной джанга сделала, мы его обсудили и ждем следующего. А это “file storage” между прочим - тоже веха!

Обновление:

Произошло одно маленькое изменение - хендлеры сигналов должны обязательно принимать **kwargs, а то получите эксепшен при попытке его присоединить

comments powered by Disqus