Twisted's Deferred - это то же самое, что и обещание в JavaScript?
Я начал использовать Twisted в проекте, который требует асинхронного программирования, и документы довольно хороши.
Итак, мой вопрос: Отложено ли в Twisted то же самое, что и Обещание в Javascript? Если нет, в чем различия?
1 ответ
Ответ на ваш вопрос - да и нет, в зависимости от того, почему вы спрашиваете.
Да:
Оба витая Deferred
и JavaScript Promise
реализовать механизм постановки в очередь синхронных блоков кода, выполняемых в заданном порядке, в то же время будучи отделенным от других синхронных блоков кода.
Нет:
Так что Javascript Promise
на самом деле больше похож на Python Future
и воздушно-сказочный способ объяснить это поговорить о Promise
и Resolver
в сочетании, чтобы сделать Deferred
и заявить, что это влияет на то, что вы можете делать с обратными вызовами.
Это все очень хорошо и хорошо в том смысле, что это точно, однако на самом деле ничего не проясняет, и, не набирая тысячи слов, в которых я почти гарантирую ошибку, я, вероятно, лучше процитирую кого-то, кто знает немного о Питоне.
Гидо ван Россум на Отложенных:
Вот моя попытка объяснить большие идеи Deferred (и их много) опытным пользователям Python без опыта Twisted. Я также предполагаю, что вы думали об асинхронных вызовах раньше. Просто чтобы раздражать Glyph, я использую 5-звездочную систему, чтобы показать важность идей, где 1 звезда - "хорошая идея, но довольно очевидная", а 5 звезд - "блестящая".
Я показываю много фрагментов кода, потому что некоторые идеи лучше всего выражены именно таким образом - но я намеренно пропускаю много деталей, и иногда я показываю код, содержащий ошибки, если их исправление уменьшит понимание идеи, лежащей в основе кода. (Я укажу на такие ошибки.) Я использую Python 3.
Примечания специально для Glyph: (а) Считайте, что это черновик для поста в блоге. Я был бы более чем рад принять исправления и предложения по улучшению. (б) это не значит, что я собираюсь изменить Tulip на более отложенную модель; но это для другой темы.
Идея 1: вернуть специальный объект вместо того, чтобы принимать аргумент обратного вызова
При разработке API, которые выдают результаты асинхронно, вы обнаружите, что вам нужна система для обратных вызовов. Обычно первое, что приходит в голову, это передать функцию обратного вызова, которая будет вызываться после завершения асинхронной операции. Я даже видел проекты, в которых, если вы не передадите обратный вызов, операция будет синхронной - это достаточно плохо, я бы дал ей ноль звездочек. Но даже версия с одной звездой загрязняет все API дополнительными аргументами, которые нужно утомительно передавать. Первая большая идея Twisted заключается в том, что лучше возвращать специальный объект, к которому вызывающий может добавить обратный вызов после его получения. Я даю это три звезды, потому что из них прорастают многие другие хорошие идеи. Это, конечно, похоже на идею, лежащую в основе Futures and Promises, которую можно найти во многих языках и библиотеках, например, в Python concurrent.futures (PEP 3148, внимательно следящий за Java Futures, оба из которых предназначены для многопоточного мира) и теперь Tulip (PEP 3156 с использованием аналогичной конструкции, адаптированной для асинхронной операции без потоков).
Идея 2: передать результаты от обратного вызова до обратного вызова
Я думаю, что лучше сначала показать код:
class Deferred: def __init__(self): self.callbacks = [] def addCallback(self, callback): self.callbacks.append(callback) # Bug here def callback(self, result): for cb in self.callbacks: result = cb(result)
Самые интересные биты - это две последние строки: результат каждого обратного вызова передается следующему. Это отличается от того, как все работает в concurrent.futures и Tulip, где результат (однажды установленный) фиксируется как атрибут будущего. Здесь результат может быть изменен каждым обратным вызовом.
Это позволяет создать новый шаблон, когда одна функция, возвращающая Deferred, вызывает другую и преобразует свой результат, и это дает этой идее три звезды. Например, предположим, что у нас есть асинхронная функция, которая читает набор закладок, и мы хотим написать асинхронную функцию, которая вызывает эту функцию, а затем сортирует закладки. Вместо того, чтобы придумывать механизм, посредством которого одна асинхронная функция может ожидать другую (что мы будем делать позже:-), вторая асинхронная функция может просто добавить новый обратный вызов к Deferred, возвращаемому первой:
def read_bookmarks_sorted(): d = read_bookmarks() d.addCallback(sorted) return d
Отложенный, возвращаемый этой функцией, представляет собой отсортированный список закладок. Если вызывающий объект хочет распечатать эти закладки, он должен добавить еще один обратный вызов:
d = read_bookmarks_sorted() d.addCallback(print)
В мире, где асинхронные результаты представлены Futures, для этого же примера потребуются два отдельных Futures: один, возвращаемый read_bookmarks(), представляющий несортированный список, и отдельный Future, возвращаемый read_bookmarks_sorted(), представляющий отсортированный список.
В этой версии класса есть одна неочевидная ошибка: если addCallback() вызывается после того, как Deferred уже сработал (т. Е. Был вызван метод callback()), то обратный вызов, добавленный addCallback(), никогда не будет вызываться. Это достаточно легко исправить, но это утомительно, и вы можете посмотреть его в исходном коде Twisted. Я расскажу об этой ошибке через последовательные примеры - просто притворитесь, что вы живете в мире, где результат никогда не будет готов слишком рано. Есть и другие проблемы с этим дизайном, но я бы скорее назвал решения улучшениями, чем исправлениями.
В стороне: Twisted плохой выбор терминологии
Я не знаю почему, но, начиная с собственного имени проекта, Twisted часто теряет меня из-за выбора названий вещей. Например, мне очень нравится принцип, что имена классов должны быть существительными. Но "Отложенный" - это прилагательное, а не просто прилагательное, это причастие глагола в прошлом (и слишком длинное при этом:-). И почему это в модуле с именем twisted.internet?
Затем есть "обратный вызов", который используется для двух связанных, но различных целей: это предпочтительный термин, используемый для функции, которая будет вызываться, когда результат будет готов, но это также имя метода, который вы вызываете для "огня". "Отложенный, т.е. установить (начальный) результат.
Не начинайте меня с неологизма / портманто, который называется "ошибка", что приводит нас к...
Идея 3: Интегрированная обработка ошибок
Эта идея получила только две звезды (что, я уверен, разочарует многих поклонников Twisted), потому что она сильно смутила меня. Я также отметил, что у Twisted docs есть некоторые проблемы с объяснением, как это работает - в этом случае, в частности, я обнаружил, что чтение кода было более полезным, чем docs.
Основная идея достаточно проста: что, если обещание уволить Отложенных с результатом не может быть выполнено? Когда мы пишем
d = pod_bay_doors.open() d.addCallback(lambda _: pod.launch())
как HAL 9000 должен сказать: "Извини, Дейв. Боюсь, я не могу этого сделать"?
И даже если мы не заботимся об этом ответе, что мы должны делать, если один из обратных вызовов вызывает исключение?
Решение Twisted состоит в том, чтобы раздавать каждый обратный вызов в обратный вызов и "ошибочный ответ". Но это еще не все - для того, чтобы иметь дело с исключениями, вызванными обратными вызовами, он также вводит новый класс 'Failure'. Я бы на самом деле хотел бы сначала представить последнее, не вводя ошибочных ответов:
class Failure: def __init__(self): self.exception = sys.exc_info()[1]
(Кстати, классное имя. И я имею в виду, я не саркастичен.)
Теперь мы можем переписать метод callback() следующим образом:
def callback(self, result): for cb in self.callbacks: try: result = cb(result) except: result = Failure()
Это само по себе я бы дал две звезды; обратный вызов может использовать isinstance(результат, отказ), чтобы отличить обычные результаты от сбоев.
Кстати, в Python 3 можно было бы покончить с отдельным классом Failure, инкапсулирующим исключения, и просто использовать встроенный класс BaseException. После чтения комментариев в коде класс Twisted Failure в основном существует, так что он может содержать всю информацию, возвращаемую sys.exc_info(), то есть класс / тип исключения, экземпляр исключения и traceback, но в Python 3 объекты исключения уже содержат ссылка на traceback. Есть некоторые вещи для отладки, которые класс Twisted Failure делает, а стандартные исключения этого не делают, но, тем не менее, я думаю, что большинство причин для введения отдельного класса были устранены.
Но давайте не будем забывать об ошибках. Мы изменяем список обратных вызовов на список пар функций обратного вызова и снова переписываем метод callback() следующим образом:
def callback(self, result): for (cb, eb) in self.callbacks: if isinstance(result, Failure): cb = eb # Use errback try: result = cb(result) except: result = Failure()
Для удобства добавим также метод errback():
def errback(self, fail=None): if fail is None: fail = Failure() self.callback(fail)
(Реальная функция errback() имеет еще несколько особых случаев, ее можно вызывать с аргументом исключения или Failure в качестве аргумента, а класс Failure принимает необязательный аргумент исключения, чтобы не использовать sys.exc_info(). это важно, и это делает фрагменты кода более сложными.)
Чтобы гарантировать, что self.callbacks является списком пар, мы также должны обновить addCallback() (он по-прежнему не работает должным образом при вызове после запуска Deferred):
def addCallback(self, callback, errback=None): if errback is None: errback = lambda r: r self.callbacks.append((callback, errback))
Если это вызывается только с помощью функции обратного вызова, errback будет фиктивной переменной, которая пропустит результат (т.е. экземпляр Failure) без изменений. Это сохраняет условие ошибки для последующего обработчика ошибок. Чтобы упростить добавление обработчика ошибок без обработки обычного результата, мы добавляем addErrback () следующим образом:
def addErrback(self, errback): self.addCallback(lambda r: r, errback)
Здесь половина обратного вызова пары передаст результат (без ошибок) через неизмененный к следующему обратному вызову.
Если вы хотите получить полную мотивацию, прочитайте Введение Twisted в Отложенные; Я просто закончу, отметив ошибку и заменив обычный результат на Failure, просто вернув значение, отличное от Failure (включая None).
Прежде чем перейти к следующей идее, позвольте мне отметить, что в настоящем классе Deferred есть больше тонкостей. Например, вы можете указать дополнительные аргументы для передачи в callback и errback. Но в крайнем случае вы можете сделать это с лямбдами, так что я опускаю это, потому что дополнительный код для администрирования не разъясняет основные идеи.
Идея 4: Отложенные цепочки
Это пятизвездочная идея! Иногда для обратного вызова действительно необходимо дождаться дополнительного асинхронного события, прежде чем он сможет дать желаемый результат. Например, предположим, что у нас есть две основные асинхронные операции, read_bookmarks() и sync_bookmarks (), и мы хотим комбинированную операцию. Если бы это был синхронный код, мы могли бы написать:
def sync_and_read_bookmarks(): sync_bookmarks() return read_bookmarks()
Но как мы это напишем, если все операции возвращают Deferreds? С идеей создания цепочки мы можем сделать это следующим образом:
def sync_and_read_bookmarks(): d = sync_bookmarks() d.addCallback(lambda unused_result: read_bookmarks()) return d
Лямбда нужна, потому что все обратные вызовы вызываются со значением результата, но read_bookmarks() не принимает аргументов.