Как я могу ждать внутри будущего объекта __await__?

PEP 0492 добавляет новый магический метод __await__. Объект, реализующий этот метод, становится объектом, подобным будущему, и его можно ожидать с помощью await. Ясно:

import asyncio


class Waiting:
    def __await__(self):
        yield from asyncio.sleep(2)
        print('ok')

async def main():
    await Waiting()

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Хорошо, но что, если я хочу назвать определенную функцию async def вместо asyncio.sleep? Я не могу использовать await, потому что __await__ не является функцией async, я не могу использовать yield from, потому что для встроенных сопрограмм требуется выражение await:

async def new_sleep():
    await asyncio.sleep(2)

class Waiting:
    def __await__(self):
        yield from new_sleep()  # this is TypeError
        await new_sleep()  # this is SyntaxError
        print('ok')

Как я могу его решить?

Ответы

Ответ 1

Использовать прямой вызов __await__():

async def new_sleep():
    await asyncio.sleep(2)

class Waiting:
    def __await__(self):
        return new_sleep().__await__()

Решение было рекомендовано Юрием Селивановым (автором PEP 492) для aioodbc library

p >

Ответ 2

Краткая версия: await foo можно заменить на yield from foo.__await__()


Объединяя все идеи из других ответов -

в простейшем случае просто делегируем другую ожидаемую работу:

def __await__(self):
    return new_sleep().__await__()

Это работает, потому что метод __await__ возвращает итератор (см. PEP 492), поэтому возвращение другого итератора __await__ - это нормально.

Это означает, конечно, что мы не можем вообще изменить поведение подвески оригинала. Более общий подход состоит в том, чтобы отразить ключевое слово await и использовать yield from - это позволяет нам объединить несколько итераторов awaitables в один:

def __await__(self):
    # theoretically possible, but not useful for my example:
    #yield from something_else_first().__await__()
    yield from new_sleep().__await__()

Здесь подвох: это не то же самое, что первый вариант! yield from является выражением, поэтому, чтобы сделать то же самое, что и раньше, нам также нужно вернуть это значение:

def __await__(self):
    return (yield from new_sleep().__await__())

Это напрямую отражает то, как мы будем писать правильное делегирование с использованием синтаксиса await:

    return await new_sleep()

лишний бит - какая разница между этими двумя?

def __await__(self):
    do_something_synchronously()
    return new_sleep().__await__()

def __await__(self):
    do_something_synchronously()
    return (yield from new_sleep().__await__())

Первый вариант - простая функция: когда вы ее вызываете, do_... и возвращается итератор. Второе - это функция генератора; вызов этого не выполняет ни один из нашего кода вообще! Только когда возвращенный итератор получен впервые do_... будет выполнен. Это имеет значение в следующей, немного надуманной ситуации:

def foo():
    tmp = Waiting.__await__()
    do_something()
    yield from tmp

Ответ 3

Я не понимал, почему я не могу уступить из native coroutine внутри __await__, но похоже, что можно получить от генератора сопрограммы внутри __await__ и вывести из native coroutine внутри этого генератора coroutine. Он работает:

async def new_sleep():
    await asyncio.sleep(2)

class Waiting:
    def __await__(self):
        @asyncio.coroutine
        def wrapper(coro):
            return (yield from coro)
        return (yield from wrapper(new_sleep()))

Ответ 4

Чтобы ждать внутри функции __await__, используйте следующий код:

async def new_sleep():
    await asyncio.sleep(1)


class Waiting:
    def __await__(self):
        yield from new_sleep().__await__()
        print('first sleep')
        yield from new_sleep().__await__()
        print('second sleep')
        return 'done'

Ответ 5

Используйте декоратор.

def chain__await__(f):
    return lambda *args, **kwargs: f(*args, **kwargs).__await__()

Затем напишите __await__ как родную сопрограмму.

async def new_sleep():
    await asyncio.sleep(2)

class Waiting:
    @chain__await__
    async def __await__(self):
        return await new_sleep()

Ответ 6

Вы также можете упростить версию Михаила до этого:

async def new_sleep():
    await asyncio.sleep(2)

class Waiting:
    def __await__(self):
        async def wrapper():
            await new_sleep()
            print("OK")
        return wrapper()

Ответ 7

Ваш оригинальный код отлично работал в Python 3.6.

Быстрое исправление (12 символов)

Исправления в других ответах впечатляют, я даже не мог поверить, что некоторые из них работают (но они делают!), Но я боюсь, что если модель asyncio изменяться, эти исправления перестанут работать.

У меня есть минималистическое исправление, которое работает на 3.7, без оболочки:

import asyncio


class Waiting:
    def __await__(self):
        yield from asyncio.sleep(2).__await__()
        print('ok')


async def main():
    await Waiting()


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Единственное отличие от вашего исходного кода заключается в добавлении .__await__() к asyncio.sleep(2) - даже без оболочки.

Подход совместимости

Вы также можете использовать эту оболочку sync_await для генератора, которого вы хотите await в __await__, вот так:

import asyncio


def sync_await(gen):
    if hasattr(gen, '__await__'):
        # 3.7, and user defined coroutines in 3.6
        print('yield from gen.__await__()')
        return (yield from gen.__await__())
    else:
        # 3.6, only native coroutines like asyncio.sleep()
        print('yield from gen')
        return (yield from gen)


class Waiting:
    def __await__(self):
        yield from sync_await(asyncio.sleep(2))
        print('ok')


async def main():
    await Waiting()


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Обратите внимание - оболочка не return (yield from...), а return (yield from...) yield from - простое делегирование итератора генератора.