На этом шаге мы рассмотрим особенности использования таких функций.
Вы пишете код, в котором используются функции обратного вызова, но вас беспокоит быстрое размножение маленьких функций и излишняя сложность потока
управления. Вы бы хотели как-то заставить код выглядеть более похожим на нормальную последовательность процедурных шагов.
Функции обратного вызова могут быть встроены в функцию путем использования генераторов и корутин (сопрограмм). Предположим, у вас есть функция, которая выполняет какую-то работу и вызывает функцию обратного вызова (см. предыдущий шаг):
def apply_async(func, args, *, callback): # Вычисляем результат result = func(*args) # Вызываем функцию обратного вызова с результатом callback(result)
Теперь взгляните на поддерживающий код, который использует класс Async и декоратор inlined_async:
from queue import Queue from functools import wraps class Async: def __init__(self, func, args): self.func = func self.args = args def inlined_async(func): @wraps(func) def wrapper(*args): f = func(*args) result_queue = Queue() result_queue.put(None) while True: result = result_queue.get() try: a = f.send(result) apply_async(a.func, a.args, callback=result_queue.put) except StopIteration: break return wrapper
Эти два фрагмента кода позволят вам встроить в строку шаги функции обратного вызова, используя инструкции yield. Например:
def add(x, y): return x + y @inlined_async def test(): r = yield Async(add, (2, 3)) print(r) r = yield Async(add, ('hello', 'world')) print(r) for n in range(10): r = yield Async(add, (n, n)) print(r) print('Goodbye')
Если вы вызовете test(), то получите такой вывод:
5
helloworld
0
2
4
6
8
10
12
14
16
18
Goodbye
Если исключить специальный декоратор и использование yield, то вы заметите, что функции обратного вызова нигде не появляются (только "под капотом").
Этот рецепт - испытание для ваших знаний в области функций обратного вызова, генераторов и потока управления.
Во-первых, основная идея кода с функциями обратного вызова в том, что текущее вычисление приостанавливается и возобновляется в какой-то момент времени позже (т. е. асинхронно). Когда вычисление возобновляется, для продолжения обработки выполняется функция обратного вызова. Функция apply_sync() иллюстрирует важнейшие составляющие выполнения функции обратного вызова, хотя в реальном мире процесс может быть намного сложнее (в нем могут использоваться потоки, процессы, обработчики событий и т. п.).
Идея того, что вычисление приостановится и возобновится, естественным образом отображается на модель выполнения генератора. Если точнее, то операция yield заставляет генератор выдавать значение и приостанавливаться. Последующие вызовы методов генератора __next__() или send() заставят его снова запуститься.
Имея это в виду, мы можем понять, что суть этого рецепта заключена в декораторе inline_async(). Главная идея в том, что декоратор пошагово проводит генератор через все его инструкции yield. Чтобы это сделать, создается очередь результатов и изначально наполняется значениями None. Затем инициируется цикл, в котором результат вынимается из очереди и посылается в генератор. Это вызывает следующий yield, где принимается экземпляр Async. Затем цикл смотрит на функцию и аргументы и вызывает асинхронное вычисление apply_sync(). Однако наиболее хитрая часть этого вычисления в том, что вместо использования обычной функции обратного вызова функция обратного вызова установлена на метод очереди put().
В этот момент остается открытым вопрос о том, что произойдет. Главный цикл немедленно возвращается наверх и просто выполняет операцию get() на очереди. Если данные присутствуют, то это должен быть результат, помещенный туда функцией обратного вызова put(). Если же ничего нет, операция блокируется и ждет, когда придет результат. Как это может произойти - зависит от конкретной реализации функции apply_async().
Если вы сомневаетесь, что такая безумная штука может работать, вы можете попробовать ее с библиотекой multiprocessing и заставить асинхронные операции выполняться в отдельных процессах:
if __name__ == '__main__': import multiprocessing pool = multiprocessing.Pool() apply_async = pool.apply_async # Запускаем тестовую функцию test()
Вы обнаружите, что это работает, но чтобы разобраться в потоке управления, вам потребуется немало кофе.
Прием скрытия нетривиального потока управления за генераторами можно найти повсеместно в стандартной библиотеке и сторонних пакетах. Например, декоратор @contextmanager из библиотеки contextlib выполняет похожий безумный фокус, который через инструкцию yield склеивает вход в менеджер контекста и выход из него. Популярный пакет Twisted тоже использует похожие встроенные функции обратного вызова.
На следующем шаге мы рассмотрим доступ к переменным, определенным внутри замыкания.