web-dev-qa-db-fra.com

Comment puis-je attendre à l'intérieur de __await__?

PEP 0492 ajoute la nouvelle méthode magique __await__. L'objet qui implémente cette méthode devient objet de type futur et peut être attendu avec await. C'est clair:

import asyncio


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

async def main():
    await Waiting()

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

Ok, mais si je veux appeler une fonction définie par async def au lieu de asyncio.sleep? Je ne peux pas utiliser await car __await__ n'est pas une fonction async, je ne peux pas utiliser yield from car les coroutines natives nécessitent await expression:

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')

Comment puis-je le résoudre?

25
Mikhail Gerasimov

Utiliser l'appel direct __await__():

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

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

La solution a été recommandée par Yury Selivanov (auteur de PEP 492 ) pour la bibliothèque aioodbc

28
Andrew Svetlov

Version abrégée: await foo peut être remplacé par yield from foo.__await__()


Combinant toutes les idées des autres réponses -

dans le cas le plus simple, il suffit de déléguer à d'autres travaux en attente:

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

Cela fonctionne car la méthode __await__ renvoie un itérateur (voir PEP 492 ), aussi, renvoyer un autre itérateur __await__ convient.

Cela signifie, bien sûr, que nous ne pouvons pas du tout changer le comportement de suspension de l'original. L’approche la plus générale consiste à reproduire le mot clé await et à utiliser yield from, ce qui nous permet de combiner plusieurs itérateurs en attente en un seul:

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

Voici le piège: cela ne fait pas exactement la même chose que la première variante! yield from est une expression, donc pour faire exactement comme avant, nous devons aussi renvoyer cette valeur:

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

Cela reflète directement comment nous écririons la délégation appropriée en utilisant la syntaxe await:

    return await new_sleep()

extra bit - quelle est la différence entre ces deux?

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

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

La première variante est une fonction simple: lorsque vous l'appelez, do_... est exécuté et un itérateur est renvoyé. La seconde est une fonction génératrice; l'appeler n'exécute aucun de nos codes! Ce n'est que lorsque l'itérateur renvoyé est généré pour la première fois que do_... sera exécuté. Cela fait une différence dans ce qui suit, une situation un peu artificielle:

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

Je ne comprenais pas pourquoi je ne pouvais pas céder avec la coroutine native dans __await__, mais il semble possible de céder avec le générateur coroutine dans __await__ et céder avec la coroutine native dans cette générateur de coroutine. Ça marche:

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
Mikhail Gerasimov

Pour attendre dans une fonction __await__, utilisez le code suivant:

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'
4
crvv

Utilisez un décorateur.

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

Ensuite, écrivez __await__ en tant que coroutine native.

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

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

Vous pouvez également simplifier la version de Mikhail à ceci:

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

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

Votre code original a bien fonctionné sous Python 3.6.

Correction rapide (12 caractères)

Les corrections dans les autres réponses sont impressionnantes, je ne pouvais même pas croire que certaines d’entre elles fonctionnaient (mais c’est le cas!), Mais je crains que si le modèle asyncio ne cesse de changer, ces corrections ne fonctionneront plus.

J'ai un correctif minimaliste, qui fonctionne sur 3.7, sans wrapper:

import asyncio


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


async def main():
    await Waiting()


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

La seule différence par rapport au code d'origine consiste à ajouter .__await__() à asyncio.sleep(2), même sans wrapper.

Approche de la compatibilité

Vous pouvez également nous utiliser ce wrapper sync_await autour du générateur que vous voulez await dans __await__, juste comme ça:

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 __== "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Remarque - le wrapper ne fait pas return (yield from ...), mais yield from - une délégation d'itérateur de générateur simple.

0
Tomasz Gandor