На этом шаге мы рассмотрим способ решения этой задачи.
Вы пишете код, который опирается на использование функций обратного вызова (например, на обработчики событий, функции обратного вызова на
завершения и т. п.), но вы хотите получить функцию обратного вызова, переносящую дополнительное состояние для использования в функции обратного вызова.
Этот рецепт относится к способу использования функций обратного вызова, который можно обнаружить во многих библиотеках и фреймворках - особенно тех, которые связаны с асинхронной обработкой. Рассмотрим следующую функцию, которая вызывает функцию обратного вызова:
>>> def apply_async(func, args, *, callback): # Вычислить результат result = func(*args) # Вызвать функцию обратного вызова с результатом callback(result)
В реальной жизни такой код может выполнять различные типы продвинутой обработки, включающей потоки, процессы и таймеры, но в данном случае это не главное. Мы просто сосредоточимся на вызове функции обратного вызова. Вот пример использования приведенного выше кода:
>>> def print_result(result): print('Got:', result) >>> def add(x, y): return x + y >>> apply_async(add, (2, 3), callback=print_result) Got: 5 >>> apply_async(add, ('hello', 'world'), callback=print_result) Got: helloworld >>>
Как вы можете видеть, функция print_result() принимает только один аргумент, который представляет собой результат. Никакая другая информация не передается. Это отсутствие информации иногда может представлять собой проблему, когда вы хотите, чтобы функция обратного вызова взаимодействовала с другими переменными или частями окружения.
Способ передать дополнительную информацию в функцию обратного вызова - это использование связанного метода вместо простой функции. Например, этот класс хранит внутренний последовательный номер, который инкрементально увеличивается каждый раз, когда получен результат:
>>> class ResultHandler: def __init__(self): self.sequence = 0 def handler(self, result): self.sequence += 1 print('[{}] Got: {}'.format(self.sequence, result)) >>>
Чтобы использовать этот класс, вы могли бы создать экземпляр и использовать связанный метод handler в качестве функции обратного вызова:
>>> r = ResultHandler() >>> apply_async(add, (2, 3), callback=r.handler) [1] Got: 5 >>> apply_async(add, ('hello', 'world'), callback=r.handler) [2] Got: helloworld >>>
В качестве альтернативы классу вы также можете использовать для хранения состояния замыкание:
>>> def make_handler(): sequence = 0 def handler(result): nonlocal sequence sequence += 1 print('[{}] Got: {}'.format(sequence, result)) return handler >>>
Вот пример использования такого варианта:
>>> handler = make_handler() >>> apply_async(add, (2, 3), callback=handler) [1] Got: 5 >>> apply_async(add, ('hello', 'world'), callback=handler) [2] Got: helloworld >>>
В качестве еще одной вариации на эту тему вы также иногда можете использовать корутину (coroutine (сопрограмма)) для выполнения той же задачи:
>>> def make_handler(): sequence = 0 while True: result = yield sequence += 1 print('[{}] Got: {}'.format(sequence, result)) >>>
Для корутины вы можете использовать метод send() в качестве функции обратного вызова:
>>> handler = make_handler() >>> next(handler) # Продвигаемся к yield >>> apply_async(add, (2, 3), callback=handler.send) [1] Got: 5 >>> apply_async(add, ('hello', 'world'), callback=handler.send) [2] Got: helloworld >>>
И последнее: вы также можете передать состояние в функцию обратного вызова, используя дополнительный аргумент и применяя функцию partial(). Например:
>>> class SequenceNo: def __init__(self): self.sequence = 0 >>> def handler(result, seq): seq.sequence += 1 print('[{}] Got: {}'.format(seq.sequence, result)) >>> seq = SequenceNo() >>> from functools import partial >>> apply_async(add, (2, 3), callback=partial(handler, seq=seq)) [1] Got: 5 >>> apply_async(add, ('hello', 'world'), callback=partial(handler, seq=seq)) [2] Got: helloworld >>>
Программы, основанные на функциях обратного вызова, часто подвержены риску превратиться в огромную беспорядочную кучу. Частично эта проблема возникает, потому что функция обратного вызова нередко отсоединена от кода, который делает первоначальный запрос, приводящий к ее выполнению. Поэтому окружение выполнения между созданием запроса и обработкой результата теряется. Если вы хотите продолжить функцию обратного вызова в процедуре из нескольких шагов, вам нужно понять, как сохранить и восстановить ассоциированное состояние.
Существует два основных подхода, которые полезны для захвата и переноса состояния. Вы можете переносить его в экземпляре (например, прикрепленном к связанному методу), или же вы можете переносить его в замыкании (вложенной функции). Из этих двух приемов замыкания, вероятно, немного более легковесны и естественны, поскольку просто создаются из функций. Они также автоматически захватывают все использованные переменные. Это освобождает вас от необходимости беспокоиться по поводу того, какое именно состояние нужно сохранить (это автоматически определяется вашим кодом).
При использовании замыканий вам нужно осторожно обращаться с изменяемыми (мутабельными) переменными. В вышеприведенном решении объявление nonlocal используется для обозначения того, что переменная sequence изменяется изнутри функции обратного вызова. Без этого объявления вы бы получили ошибку.
Использование корутины (сопрограммы) в качестве обработчика функций обратного вызова интересно тем, что это тесно связано с подходом с использованием замыканий. В некотором смысле он даже прозрачнее, поскольку представляет собой одну функцию. Более того, переменные можно свободно изменять и не беспокоиться об объявлениях nonlocal. Потенциальный недостаток в том, что корутины не так легко понять, как другие компоненты Python. Есть также несколько тонких моментов - таких как необходимость вызывать next() на корутине, перед тем как ее использовать. Тем не менее корутины можно использовать и по-другому - например, для определения встроенной функции обратного вызова (см. следующий рецепт).
Последний прием с использованием partial() полезен, если вам нужно просто передать дополнительные значения в функцию обратного вызова. Иногда вместо partial() мы можем достичь того же с помощью lambda:
>>> apply_async(add, (2, 3), callback=lambda r: handler(r, seq)) [3] Got: 5 >>>
Дополнительные примеры мы можете найти на 124 шаге, где показывается использование partial() для изменения аргументных сигнатур.
На следующем шаге мы рассмотрим встроенные функции обратного вызова.