Шаг 127.
Python: сборник рецептов.
Функции. Встроенные функции обратного вызова

    На этом шаге мы рассмотрим особенности использования таких функций.

Задача

    Вы пишете код, в котором используются функции обратного вызова, но вас беспокоит быстрое размножение маленьких функций и излишняя сложность потока управления. Вы бы хотели как-то заставить код выглядеть более похожим на нормальную последовательность процедурных шагов.

Решение

    Функции обратного вызова могут быть встроены в функцию путем использования генераторов и корутин (сопрограмм). Предположим, у вас есть функция, которая выполняет какую-то работу и вызывает функцию обратного вызова (см. предыдущий шаг):

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 тоже использует похожие встроенные функции обратного вызова.

    На следующем шаге мы рассмотрим доступ к переменным, определенным внутри замыкания.




Предыдущий шаг Содержание