web-dev-qa-db-fra.com

Comment exécuter une coroutine en dehors d'une boucle d'événement?

Donc, généralement, vous saisissez le résultat d'une coroutine en faisant quelque chose comme ceci:

async def coro():
    await asycnio.sleep(3)
    return 'a value'

loop = asyncio.get_event_loop()
value = loop.run_until_complete(coro())

Par curiosité, quelle est la manière la plus simple d'obtenir cette valeur sans utiliser de boucle d'événement?

[ÉDITER]

Je pense qu'une manière encore plus simple peut être:

async def coro():
    ...

value = asyncio.run(coro())  # Python 3.7+

Mais existe-t-il un moyen de trier yield from (Ou await) une coro() globalement comme dans JS ? Sinon, pourquoi?

13
Charming Robot

Il y a ici deux questions: l'une concerne l'attente d'une coroutine "au plus haut niveau", ou plus concrètement dans un environnement de développement. L'autre consiste à exécuter une coroutine sans boucle d'événement.

En ce qui concerne la première question, cela est certainement possible en Python, tout comme cela est possible dans Chrome Canary Dev Tools - par l'outil qui le gère via sa propre intégration avec la boucle d'événements. Et en effet, IPython 7.0 et versions ultérieures prennent en charge asyncio nativement et vous pouvez utiliser await coro() au niveau supérieur comme prévu.

Concernant la deuxième question, il est facile de piloter une seule coroutine sans boucle d'événement, mais ce n'est pas très utile. Voyons pourquoi.

Lorsqu'une fonction coroutine est appelée, elle renvoie un objet coroutine. Cet objet est démarré et repris en appelant sa méthode send(). Lorsque la coroutine décide de suspendre (car elle awaits quelque chose qui bloque), send() retournera. Lorsque la coroutine décide de retourner (parce qu'elle a atteint la fin ou parce qu'elle a rencontré un return explicite), elle lève un StopIteration exception avec l'attribut value défini sur la valeur de retour. Dans cet esprit, un pilote minimal pour une seule coroutine pourrait ressembler à ceci:

def drive(c):
    while True:
        try:
            c.send(None)
        except StopIteration as e:
            return e.value

Cela fonctionnera très bien pour les coroutines simples:

>>> async def pi():
...     return 3.14
... 
>>> drive(pi())
3.14

Ou même pour les plus complexes:

>>> async def plus(a, b):
...     return a + b
... 
>>> async def pi():
...     val = await plus(3, 0.14)
...     return val
... 
>>> drive(pi())
3.14

Mais quelque chose manque toujours - aucune des coroutines ci-dessus n'a jamais suspendu leur exécution. Lorsqu'une coroutine est suspendue, elle permet à d'autres coroutines de s'exécuter, ce qui permet à la boucle d'événements d'exécuter (semble-t-il) plusieurs coroutines à la fois. Par exemple, asyncio a une coroutine sleep() qui, lorsqu'elle est attendue, suspend l'exécution pendant la période spécifiée:

async def wait(s):
    await asyncio.sleep(1)
    return s

>>> asyncio.run(wait("hello world"))
'hello world'      # printed after a 1-second pause

Cependant, drive ne parvient pas à exécuter cette coroutine à la fin:

>>> drive(wait("hello world"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in drive
  File "<stdin>", line 2, in wait
  File "/usr/lib/python3.7/asyncio/tasks.py", line 564, in sleep
    return await future
RuntimeError: await wasn't used with future

Ce qui s'est passé, c'est que sleep() communique avec la boucle d'événements en produisant un objet "futur" spécial. Une coroutine en attente d'un avenir ne peut être reprise qu'après que l'avenir a été fixé. La boucle d'événements "réelle" le ferait en exécutant d'autres coroutines jusqu'à ce que l'avenir soit fait.

Pour résoudre ce problème, nous pouvons écrire notre propre implémentation sleep qui fonctionne avec notre mini boucle d'événements. Pour ce faire, nous devons utiliser un itérateur pour implémenter l'attendu:

class my_sleep:
    def __init__(self, d):
        self.d = d
    def __await__(self):
        yield 'sleep', self.d

Nous produisons un Tuple qui ne sera pas vu par l'appelant coroutine, mais dira à drive (notre boucle d'événement) quoi faire. drive et wait ressemblent maintenant à ceci:

def drive(c):
    while True:
        try:
            susp_val = c.send(None)
            if susp_val is not None and susp_val[0] == 'sleep':
                time.sleep(susp_val[1])
        except StopIteration as e:
            return e.value

async def wait(s):
    await my_sleep(1)
    return s

Avec cette version, wait fonctionne comme prévu:

>>> drive(wait("hello world"))
'hello world'

Ce n'est toujours pas très utile car la seule façon de piloter notre coroutine est d'appeler drive(), qui supporte à nouveau une seule coroutine. Donc, nous aurions aussi bien pu écrire une fonction synchrone qui appelle simplement time.sleep() et l'appelle un jour. Pour que nos coroutines prennent en charge le cas d'utilisation de la programmation asynchrone, drive() devrait:

  • soutenir le fonctionnement et la suspension de plusieurs coroutines
  • implémenter le frai de nouvelles coroutines dans la boucle d'entraînement
  • permettre aux coroutines d'enregistrer des réveils sur des événements liés aux E/S, tels qu'un descripteur de fichier devenant lisible ou inscriptible - tout en prenant en charge plusieurs de ces événements sans perte de performances

C'est ce que la boucle d'événements asyncio apporte à la table, ainsi que de nombreuses autres fonctionnalités. Construire une boucle d'événement à partir de zéro est superbement démontré dans cet exposé par David Beazley, où il implémente une boucle d'événement fonctionnelle devant un public en direct.

10
user4815162342

Donc, après un peu de fouille, je pense avoir trouvé la solution la plus simple pour exécuter une coroutine à l'échelle mondiale.

Si vous >>> dir(coro) Python affichera les attributs suivants:

['__await__', '__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'cr_await', 'cr_code', 'cr_frame', 'cr_Origin', 'cr_running', 'send', 'throw']

Quelques attributs se distinguent, à savoir:

[
   '__await__',
   'close',
   'cr_await',
   'cr_code',
   'cr_frame',
   'cr_Origin',
   'cr_running',
   'send',
   'throw'
]

Après avoir lu que fait yield (yield)? et généralement comment fonctionnent les générateurs, j'ai pensé que la méthode d'envoi devait être la clé.

J'ai donc essayé de:

>>> the_actual_coro = coro()
<coroutine object coro at 0x7f5afaf55348> 

>>>the_actual_coro.send(None)

Et cela a soulevé une erreur intéressante:

Original exception was:
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration: a value

Cela m'a en fait rendu la valeur de retour dans une exception !

J'ai donc pensé qu'une boucle très basique, eh bien, c'est plus un coureur, peut être implémentée comme telle:

def run(coro):
    try:
        coro.send(None)
    except StopIteration as e:
        return e.value

Maintenant, je peux exécuter une coroutine dans une fonction de synchronisation, ou même globalement, pas que je recommanderais de le faire. Mais, il est intéressant de connaître le niveau le plus simple et le plus bas que vous pouvez utiliser pour exécuter une coroutine

>>> run(coro())
'a value'

Cela renvoie cependant None lorsque le coro a quelque chose à attendre (ce qui est vraiment l'essence même d'être une coroutine).

Je pense que c'est probablement parce que la boucle d'événement gère les attentes de ses coroutines (coro.cr_frame.f_locals) en les affectant à des contrats à terme et en les gérant séparément? que ma simple fonction run ne fournit évidemment pas. Je me trompe peut-être à cet égard. Alors s'il vous plaît, corrigez-moi si je me trompe.

3
Charming Robot

Il n'y a aucun moyen d'obtenir la valeur de coroutine sans utiliser de boucle d'événement car la coroutine ne peut être exécutée que par une boucle d'événement.

Cependant, vous pouvez exécuter une coroutine sans la transmettre explicitement à run_until_complete. Vous pouvez simplement l'attendre pour obtenir de la valeur pendant l'exécution de la boucle d'événements. Par exemple:

import asyncio


async def test():
    await asyncio.sleep(1)
    return 'a value'


async def main():
    res = await test()
    print('got value from test() without passing it to EL explicitly')
    print(res)


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
3
Mikhail Gerasimov