Вчера джанга сделал ещё один существенный шаг на пути к долгожданному релизу 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, а то получите эксепшен при попытке его присоединить
Комментарии 13
С замиранием сердца слежу за предрелизными django'трясениями. И после каждого сначала ликую, а потом с ужасом оцениваю "причинённый ущерб" :) Сколько часов теперь пройдут в рефакторинге написанного за последние пару лет... ужас! Теперь вот ещё и завязанный на сигналы код...
И всё же, я рад! :)
Спасибо за статью и хорошие новости!
Оставлен 07 Август 2008 в 16:56 ¶"Первое что бросается в гала, это то что сами сигналы теперь полноценный классы" - слющай, дарагой, сыгнал харащо писать, да?
А за статью спасибо :)
Оставлен 07 Август 2008 в 17:54 ¶typo: Хотя многие разработчики и не особо их используют в своём коже...
Оставлен 07 Август 2008 в 18:17 ¶Эх, да. Спасибо.
Оставлен 07 Август 2008 в 18:56 ¶А может http://orphus.ru/? Чтобы не писать про мелкие ляпы в комментариях.
Оставлен 07 Август 2008 в 19:53 ¶Не:) Мне больше другие варианты нравятся:
-Прикрутить typo reporter к движку блога
-Или прокачать как-то скил typo resistance
Но серьезно говоря, просто надо ещё один лишний раз прочитывать - что и буду делать:)
Оставлен 07 Август 2008 в 20:29 ¶Обновление:
Произошло одно маленькое изменение - хендлеры сигналов должны обязательно принимать
Оставлен 08 Август 2008 в 13:19 ¶**kwargs, а то получите эксепшен при попытке его присоединитьИнтересно, зачем им понадобилось тащить из pyDispatcher saferef.py, если есть стандартный weakref?
Оставлен 10 Сентябрь 2008 в 16:50 ¶Там в комментах:
Оставлен 11 Сентябрь 2008 в 10:15 ¶Понятно.
Оставлен 11 Сентябрь 2008 в 10:38 ¶Мне сигналы не очень нравятся, потому что снижают ясность кода. Непонятно, что будет вызвано при конкретном событии. Я один такой?
Оставлен 02 Декабрь 2008 в 22:18 ¶Наверно нет, но сигналы очень распространенный паттерн взаимодействия компонентов систем во многих областях. Зачастую они наоборот упрощают код.
Оставлен 03 Декабрь 2008 в 00:03 ¶Понятно что упрощают :) Но меня очень смущает мысль об отладке кода с неочевидными зависимостями, когда неизвестно, что (и в каком порядке) будет вызвано в результате каких-то действий. Может просто привыкнуть надо...
Оставлен 03 Декабрь 2008 в 00:37 ¶Комментирование данного поста закрыто.