web-dev-qa-db-fra.com

Comment puis-je exécuter périodiquement une fonction avec asyncio?

Je migre de tornado vers asyncio, et je ne trouve pas l'équivalent asyncio de tornado de PeriodicCallback. (Un PeriodicCallback prend deux arguments: la fonction à exécuter et le nombre de millisecondes entre les appels.)

  • Existe-t-il un tel équivalent dans asyncio?
  • Sinon, quel serait le moyen le plus propre d'implémenter cela sans courir le risque d'obtenir un RecursionError après un certain temps?
45
2Cubed

Pour Python versions antérieures à 3.5:

import asyncio

@asyncio.coroutine
def periodic():
    while True:
        print('periodic')
        yield from asyncio.sleep(1)

def stop():
    task.cancel()

loop = asyncio.get_event_loop()
loop.call_later(5, stop)
task = loop.create_task(periodic())

try:
    loop.run_until_complete(task)
except asyncio.CancelledError:
    pass

Pour Python 3.5 et supérieur:

import asyncio

async def periodic():
    while True:
        print('periodic')
        await asyncio.sleep(1)

def stop():
    task.cancel()

loop = asyncio.get_event_loop()
loop.call_later(5, stop)
task = loop.create_task(periodic())

try:
    loop.run_until_complete(task)
except asyncio.CancelledError:
    pass
35

Lorsque vous sentez que quelque chose doit arriver "en arrière-plan" de votre programme asyncio, asyncio.Task pourrait être un bon moyen de le faire. Vous pouvez lire cet article pour voir comment travailler avec des tâches.

Voici l'implémentation possible de la classe qui exécute une fonction périodiquement:

import asyncio
from contextlib import suppress


class Periodic:
    def __init__(self, func, time):
        self.func = func
        self.time = time
        self.is_started = False
        self._task = None

    async def start(self):
        if not self.is_started:
            self.is_started = True
            # Start task to call func periodically:
            self._task = asyncio.ensure_future(self._run())

    async def stop(self):
        if self.is_started:
            self.is_started = False
            # Stop task and await it stopped:
            self._task.cancel()
            with suppress(asyncio.CancelledError):
                await self._task

    async def _run(self):
        while True:
            await asyncio.sleep(self.time)
            self.func()

Testons le:

async def main():
    p = Periodic(lambda: print('test'), 1)
    try:
        print('Start')
        await p.start()
        await asyncio.sleep(3.1)

        print('Stop')
        await p.stop()
        await asyncio.sleep(3.1)

        print('Start')
        await p.start()
        await asyncio.sleep(3.1)
    finally:
        await p.stop()  # we should stop task finally


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

Sortie:

Start
test
test
test

Stop

Start
test
test
test

[Finished in 9.5s]

Comme vous le voyez sur start, nous commençons simplement la tâche qui appelle certaines fonctions et dort un certain temps en boucle infinie. Sur stop, nous venons d'annuler cette tâche. Notez que cette tâche doit être arrêtée au moment où le programme est terminé.

Une autre chose importante est que votre rappel ne devrait pas prendre beaucoup de temps à être exécuté (sinon il gèlera votre boucle d’événements). Si vous prévoyez d'appeler des func de longue durée, vous aurez probablement besoin de pour les exécuter dans l'exécuteur .

19

Il n'y a pas de support intégré pour les appels périodiques, non.

Créez simplement votre propre boucle de planificateur qui dort et exécute toutes les tâches planifiées:

import math, time

async def scheduler():
    while True:
        # sleep until the next whole second
        now = time.time()
        await asyncio.sleep(math.ceil(now) - now)

        # execute any scheduled tasks
        await for task in scheduled_tasks(time.time()):
            await task()

L'itérateur scheduled_tasks() doit générer des tâches prêtes à être exécutées à l'heure indiquée. Notez que la production de l’horaire et le lancement de toutes les tâches peuvent en principe durer plus d’une seconde; L'idée ici est que le planificateur génère toutes les tâches qui auraient dû être lancées depuis le dernier contrôle.

15
Martijn Pieters

Version alternative avec décorateur pour python 3.7

import asyncio
import time


def periodic(period):
    def scheduler(fcn):

        async def wrapper(*args, **kwargs):

            while True:
                asyncio.create_task(fcn(*args, **kwargs))
                await asyncio.sleep(period)

        return wrapper

    return scheduler


@periodic(2)
async def do_something(*args, **kwargs):
    await asyncio.sleep(5)  # Do some heavy calculation
    print(time.time())


if __== '__main__':
    asyncio.run(do_something('Maluzinha do papai!', secret=42))

Une variante qui peut être utile: si vous souhaitez que votre appel périodique se produise toutes les n secondes au lieu de n secondes entre la fin de la dernière exécution et le début de la suivante et que vous ne voulez pas que les appels se chevauchent, procédez comme suit: est plus simple:

async def repeat(interval, func, *args, **kwargs):
    """Run func every interval seconds.

    If func has not finished before *interval*, will run again
    immediately when the previous iteration finished.

    *args and **kwargs are passed as the arguments to func.
    """
    while True:
        await asyncio.gather(
            func(*args, **kwargs),
            asyncio.sleep(interval),
        )

Et un exemple d'utilisation de ce logiciel pour exécuter quelques tâches en arrière-plan:

async def f():
    await asyncio.sleep(1)
    print('Hello')


async def g():
    await asyncio.sleep(0.5)
    print('Goodbye')


async def main():
    t1 = asyncio.ensure_future(repeat(3, f))
    t2 = asyncio.ensure_future(repeat(2, g))
    await t1
    await t2

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
1
Fred Ross

Basé sur @A. Réponse de Jesse Jiryu Davis (avec les commentaires de @Torkel Bjørnson-Langen et @ReWrite), il s'agit d'une amélioration qui évite la dérive.

import time
import asyncio

@asyncio.coroutine
def periodic(period):
    def g_tick():
        t = time.time()
        count = 0
        while True:
            count += 1
            yield max(t + count * period - time.time(), 0)
    g = g_tick()

    while True:
        print('periodic', time.time())
        yield from asyncio.sleep(next(g))

loop = asyncio.get_event_loop()
task = loop.create_task(periodic(1))
loop.call_later(5, task.cancel)

try:
    loop.run_until_complete(task)
except asyncio.CancelledError:
    pass
1
Wojciech Migda