На днях у меня в форуме возникли почти подряд два топика про сигналы. Оба напомнили мне давно закравшееся в голове подозрение о том, что для многих сигналы — это магия из серии "если что-то не выходит, наверняка для этого нужны сигналы" :-).
Хочу раскрыть тему, потому как рамки форумного ответа особо разгуляться не позволяют.
Сигналы синхронны
Часто совет (неверный) использовать сигналы встречается в ситуации, когда нужно в веб-запросе сделать какую-то потенциально долгую операцию, и хочется, чтобы она выполнилась как-нибудь в фоне (асинхронно, то есть), чтобы пользователь получил уже какой-нибудь ответ до того, как эта операция завершить.
Но сигналы тут совсем ни причем. Не знаю, откуда берется идея, что они выполняются в каком-то другом времени. Возможно из-за того, что механизм их работы где-то там в глубине скрыт, и просто хочется верить, что сделано так как раз для асинхронности (а для чего ещё?). А может виной всему опыт использования POSIX'овых сигналов, которые присылаются ОС в некие произвольные моменты времени и с точки зрения программы действительно происходят асинхронно с её процессом исполнения.
На самом же деле механизм вызова сигнала по сути своей прост, как доска. Точнее, как цикл "for". Представьте, что у вас где-то в системе есть глобальный словарик такого рода:
signals = {
signal1: [func1, func2],
signal2: [func3],
}
И вызов например signal1.send(*args)
— это:
for func in signals[signal1]:
func(*args)
Вот и всё. И пока весь цикл не пройдёт и все обработчики не отработают, вызов "send" будет честно их ждать.
Слабая связанность компонентов
Разобравшись с тем, чего сигналы не делают, осталось понять, зачем нужен такой странный способ вызывать функции.
Представим для примера некий гипотетический Форум, в котором есть авторизация по OpenID. Когда в форум приходит новый пользователь, код авторизации заводит новую запись форумного Профиля для этого пользователя. Делает он это самым простым и очевидным способом: импортирует модель Profile и создает её новый объект.
Теперь представим, что нам хочется оторвать авторизацию по OpenID в отдельное приложение — некий гипотетический Правильный OpenID-консумер. Просто перенести весь авторизационный код в отдельную директорию недостаточно из-за сильной связности: Авторизация всё ещё импортирует к себе модель Форума Profile, а значит её нельзя будет использовать как отдельное приложение без Форума.
Это как раз и решается сигналами. Вместо того, чтобы звать Форум напрямую, Авторизация в момент осознания, что с ней случился новый пользователь, отсылает в Бесконечный Эфир сигнал. Это специальный сигнал, описанный специально для такой оказии где-то в коде Авторизации и основательно документирован. Со своей стороны Форум импортирует определение сигнала из Авторизации и начинает его слушать, повесив на него в качестве реакции свою функцию, создающую новому пользователю профиль.
Таким образом мы получаем связь с однонаправленной зависимостью:
Форум сильно связан с (зависит от) Авторизацией, потому что должен знать про то, что она отсылает этот конкретный сигнал. Тто есть должен его явно импортировать.
Авторизация слабо связана с (не зависит от) Форума, потому что предоставляет свой сигнал для любого потребителя и благодаря тому, что те навешивают обработчики сигналам по собственной инициативе, она не зависит от их наличия, количества или сущности.
Вот эта слабая связность и есть то, для чего в первую очередь существуют сигналы. Она позволяет слабо связному компоненту разрабатываться отдельно и гарантированно не ломать систему до тех пор, пока соблюдается документированный протокол сигнала.
P.S. Создание системы со слабой связностью в обоих направлениях остаётся в качестве упражнения читателю.
Комментарии: 16
С интересом прочитаю пример системы со слабой связностью в обоих направлениях. Мне видится затык в одном месте. Чтобы форуму начать ловить сигнал от авторизации, этот сигнал надо импортировать. Импорт - это сильная связность. Как избавиться от импорта - не понимаю. Читатели, help!
Цепляться на сигнал post_save у самого юзера, а не у профиля. Но это только переносит связку уровнем выше.
То есть сигналы - это просто реализация паттерна Observer.
Ага, точно.
Только (как к сожалению бывает в Питоне) крайне неудачно выбрано имя. Listener в данном контексте куда лучше и точнее.
На самом деле, сам модуль называется dispatcher (pydispatcher в доджанговской инкарнации), и это правомерный синоним паттерна наряду с Observer и Listener. Но вот народ как-то больше полюбил называть это словом сигналы.
А зачем его ловить? Форум также может дать некий абстрактный интерфейс взаимодействия со сторонними приложениями авторизации и не быть завязанным на какую-то конкретно.
Конечно связывание этого приложения авторизации и этого приложения форума может происходить как на уровне проекта, так и в каком-то третьем приложении.
Бесконечный Эфир порадовал, но если бы был Мировой Эфир, вообще бы здорово было ;-)
Названий у этого паттерна дофига. Publisher-Subscriber, Signall-Slot, просто Signals, Observer - всё это практически одно и то же. А асинхронные - это Deferred :)
@isagalaev
Странно, что это всё нужно рассказывать питонистам - мне казалось, что в питон уходят люди, обычно имеющие уже некоторую теоретическую базу.
Сергей, тут есть три соображения.
Во-первых, времена меняются, и многие приходят в Питон без какой бы то ни было базы. Отчасти из-за роста популярности языка, отчасти их привлекают развитые прикладные средства, вроде Джанги. По многим вопросам в джанговских форумах и maillist'ах видно, что люди пришли делать сайты на Джанге, а Питон и программирование, как таковое, изучают уже по ходу.
Во-вторых, я настойчиво пытаюсь избавляться от чувства, что "это и так уже все знают". На самом деле это не так, у каждого из нас есть какое-то количество пробелов в знаниях, которые и восполняются такими вот случайными статьями в блогах. Напомню, что пост начался именно с того, что подобные вопросы реально возникли у людей.
Ну а в-третьих, я посты часто пишу порывом. Сложилось какие-то мысли в голове, вот и написал. :-)
Такой велосипед уже придуман и называется "Обработчик сигналов"...)
Иван, не могли бы вы привести практический пример задачи с системой со слабой связностью в обоих направлениях?..
Ага, это частный случай.
Ну лично мне кажется, что это надо, когда вы хотите написать два по сути независимых приложения, но которые могут работать вместе.
В принципе простейший выход в данной ситуации - простейший промежуточный модуль в котором определены только сигналы. Модуль сам зависит только от Django, а оба приложения зависят от модуля.
Я сейчас делаю такое решения для системы приёма платежей: сколько и каких будет подключено платёжных систем я не знаю, но факт ввода денег я должен всегда видеть в биллинге. Естественно биллинг ни в коем случае не должен зависеть от платёжных систем, а они от биллинга.
По поводу паттернов и их названий вспоминается презентация Joe Gregorio под названием (The Lack of) Design Patterns in Python (в сети есть и пдф, и видео). В частности:
Мне кажется, немалая причина того, что многие думают об асинхронности сигналов в том, что разработчики думают о них с событиями в JavaScript.