web-dev-qa-db-fra.com

Quel est le meilleur moyen d'exécuter plusieurs fois une fonction toutes les x secondes en Python?

Je veux exécuter de manière répétée une fonction en Python toutes les 60 secondes (comme un NSTimer dans Objective C). Ce code fonctionnera en tant que démon et ressemblera en réalité à l'appel du script python toutes les minutes à l'aide d'un cron, mais sans que cela soit configuré par l'utilisateur.

Dans cette question sur un cron implémenté en Python , la solution semble effectivement juste sleep () pendant x secondes. Je n'ai pas besoin d'une fonctionnalité aussi avancée, alors peut-être qu'un tel système fonctionnerait

while True:
    # Code executed here
    time.sleep(60)

Y at-il des problèmes prévisibles avec ce code?

191
DavidM

Utilisez le module sched , qui implémente un planificateur d'événements à usage général.

import sched, time
s = sched.scheduler(time.time, time.sleep)
def do_something(sc): 
    print "Doing stuff..."
    # do your stuff
    s.enter(60, 1, do_something, (sc,))

s.enter(60, 1, do_something, (s,))
s.run()
176
nosklo

Verrouillez simplement votre boucle de temps sur l'horloge système. Facile.

import time
starttime=time.time()
while True:
  print "tick"
  time.sleep(60.0 - ((time.time() - starttime) % 60.0))
120
Dave Rove

Vous voudrez peut-être envisager Twisted qui est une bibliothèque de réseau Python qui implémente le Reactor Pattern .

from twisted.internet import task, reactor

timeout = 60.0 # Sixty seconds

def doWork():
    #do work here
    pass

l = task.LoopingCall(doWork)
l.start(timeout) # call every sixty seconds

reactor.run()

Twisted implémente probablement déjà de nombreuses fonctionnalités dont vous aurez éventuellement besoin (démonisation, journalisation ou gestion des exceptions comme indiqué par bobince) et constituera probablement une solution plus robuste.

57
Aaron Maenpaa

Si vous voulez un moyen non bloquant d'exécuter votre fonction périodiquement, au lieu d'une boucle infinie bloquante, j'utiliserais un minuteur fileté. De cette façon, votre code peut continuer à s'exécuter, effectuer d'autres tâches et toujours appeler votre fonction toutes les n secondes. J'utilise beaucoup cette technique pour imprimer des informations de progression sur des tâches longues, intensives en CPU/disque/réseau.

Voici le code que j'ai posté dans une question similaire, avec les contrôles start () et stop ():

from threading import Timer

class RepeatedTimer(object):
    def __init__(self, interval, function, *args, **kwargs):
        self._timer     = None
        self.interval   = interval
        self.function   = function
        self.args       = args
        self.kwargs     = kwargs
        self.is_running = False
        self.start()

    def _run(self):
        self.is_running = False
        self.start()
        self.function(*self.args, **self.kwargs)

    def start(self):
        if not self.is_running:
            self._timer = Timer(self.interval, self._run)
            self._timer.start()
            self.is_running = True

    def stop(self):
        self._timer.cancel()
        self.is_running = False

Usage:

from time import sleep

def hello(name):
    print "Hello %s!" % name

print "starting..."
rt = RepeatedTimer(1, hello, "World") # it auto-starts, no need of rt.start()
try:
    sleep(5) # your long-running job goes here...
finally:
    rt.stop() # better in a try/finally block to make sure the program ends!

Caractéristiques:

  • Bibliothèque standard uniquement, pas de dépendances externes
  • start() et stop() peuvent appeler plusieurs fois même si le chronomètre a déjà démarré/arrêté
  • la fonction à appeler peut avoir des arguments positionnels et nommés
  • Vous pouvez changer interval à tout moment, il sera effectif après la prochaine exécution. Idem pour args, kwargs et même function!
41
MestreLion

Le moyen le plus simple je crois d’être:

import time

def executeSomething():
    #code here
    time.sleep(60)

while True:
    executeSomething()

De cette façon, votre code est exécuté, puis il attend 60 secondes, puis il s'exécute à nouveau, attend, exécute, etc ....__ Pas besoin de compliquer les choses.

30
Itxaka

Voici une mise à jour du code de MestreLion qui évite de dériver dans le temps:

import threading 
import time

class RepeatedTimer(object):
  def __init__(self, interval, function, *args, **kwargs):
    self._timer = None
    self.interval = interval
    self.function = function
    self.args = args
    self.kwargs = kwargs
    self.is_running = False
    self.next_call = time.time()
    self.start()

  def _run(self):
    self.is_running = False
    self.start()
    self.function(*self.args, **self.kwargs)

  def start(self):
    if not self.is_running:
      self.next_call += self.interval
      self._timer = threading.Timer(self.next_call - time.time(), self._run)
      self._timer.start()
      self.is_running = True

  def stop(self):
    self._timer.cancel()
    self.is_running = False
14
eraoul
import time, traceback

def every(delay, task):
  next_time = time.time() + delay
  while True:
    time.sleep(max(0, next_time - time.time()))
    try:
      task()
    except Exception:
      traceback.print_exc()
      # in production code you might want to have this instead of course:
      # logger.exception("Problem while executing repetitive task.")
    # skip tasks if we are behind schedule:
    next_time += (time.time() - next_time) // delay * delay + delay

def foo():
  print("foo", time.time())

every(5, foo)

Si vous voulez faire cela sans bloquer votre code restant, vous pouvez l'utiliser pour le laisser s'exécuter dans son propre thread:

import threading
threading.Thread(target=lambda: every(5, foo)).start()

Cette solution combine plusieurs fonctionnalités rarement combinées dans les autres solutions:

  • Gestion des exceptions: Dans la mesure du possible à ce niveau, les exceptions sont gérées correctement, i. e. être connecté à des fins de débogage sans abandonner notre programme.
  • Pas de chaînage: L'implémentation commune à la chaîne (pour la planification du prochain événement) que vous trouvez dans de nombreuses réponses est fragile en ce sens que si quelque chose ne va pas dans le mécanisme de planification (threading.Timer ou autre), cela mettra fin à la chaîne . Aucune autre exécution ne se produira alors, même si la raison du problème est déjà corrigée. Une simple boucle et attendre avec une simple sleep() est beaucoup plus robuste en comparaison.
  • Pas de dérive: Ma solution garde une trace exacte des heures auxquelles elle est censée fonctionner. Il n'y a pas de dérive en fonction du temps d'exécution (comme dans beaucoup d'autres solutions).
  • Saut: Ma solution sautera des tâches si une exécution prenait trop de temps (par exemple, faire X toutes les cinq secondes, mais X a pris six secondes). C'est le comportement standard de cron (et pour une bonne raison). De nombreuses autres solutions exécutent simplement la tâche plusieurs fois de suite sans aucun délai. Dans la plupart des cas (tâches de nettoyage, par exemple), cela n'est pas souhaité. Si est souhaité, utilisez simplement next_time += delay à la place.
10
Alfe

J'ai rencontré un problème similaire il y a quelque temps. Peut-être http://cronus.readthedocs.org pourrait aider?

Pour la version 0.2, l'extrait suivant fonctionne

import cronus.beat as beat

beat.set_rate(2) # 2 Hz
while beat.true():
    # do some time consuming work here
    beat.sleep() # total loop duration would be 0.5 sec
5
Anay

La principale différence entre cela et cron est qu'une exception tue définitivement le démon. Vous voudrez peut-être envelopper avec un capteur d'exception et un enregistreur.

4
bobince

Une réponse possible:

import time
t=time.time()

while True:
    if time.time()-t>10:
        #run your task here
        t=time.time()
1
sks

par exemple, afficher l'heure locale actuelle

import datetime
import glib
import logger

def get_local_time():
    current_time = datetime.datetime.now().strftime("%H:%M")
    logger.info("get_local_time(): %s",current_time)
    return str(current_time)

def display_local_time():
    logger.info("Current time is: %s", get_local_time())
    return True

# call every minute
glib.timeout_add(60*1000, display_local_time)
0
rise.riyo
    ''' tracking number of times it prints'''
import threading

global timeInterval
count=0
def printit():
  threading.Timer(timeInterval, printit).start()
  print( "Hello, World!")
  global count
  count=count+1
  print(count)
printit

if __== "__main__":
    timeInterval= int(input('Enter Time in Seconds:'))
    printit()
0
raviGupta

J'utilise la méthode after () de Tkinter, qui ne "vole pas le jeu" (comme le module sched présenté précédemment), c'est-à-dire qu'elle permet à d'autres opérations de s'exécuter en parallèle:

import Tkinter

def do_something1():
  global n1
  n1 += 1
  if n1 == 6: # (Optional condition)
    print "* do_something1() is done *"; return
  # Do your stuff here
  # ...
  print "do_something1() "+str(n1)
  tk.after(1000, do_something1)

def do_something2(): 
  global n2
  n2 += 1
  if n2 == 6: # (Optional condition)
    print "* do_something2() is done *"; return
  # Do your stuff here
  # ...
  print "do_something2() "+str(n2)
  tk.after(500, do_something2)

tk = Tkinter.Tk(); 
n1 = 0; n2 = 0
do_something1()
do_something2()
tk.mainloop()

do_something1() et do_something2() peuvent fonctionner en parallèle et quelle que soit la vitesse d'intervalle. Ici, le deuxième sera exécuté deux fois plus vite. Notez également que j’ai utilisé un simple compteur comme condition pour terminer l’une ou l’autre des fonctions. Vous pouvez utiliser n’importe quelle autre concurrence ou aucune autre si vous souhaitez exécuter une fonction jusqu’à ce que le programme se termine (par exemple, une horloge). 

0
Apostolos

J'utilise ceci pour provoquer 60 événements par heure, la plupart des événements se produisant au même nombre de secondes après la minute entière:

import math
import time
import random

TICK = 60 # one minute tick size
TICK_TIMING = 59 # execute on 59th second of the tick
TICK_MINIMUM = 30 # minimum catch up tick size when lagging

def set_timing():

    now = time.time()
    elapsed = now - info['begin']
    minutes = math.floor(elapsed/TICK)
    tick_elapsed = now - info['completion_time']
    if (info['tick']+1) > minutes:
        wait = max(0,(TICK_TIMING-(time.time() % TICK)))
        print ('standard wait: %.2f' % wait)
        time.sleep(wait)
    Elif tick_elapsed < TICK_MINIMUM:
        wait = TICK_MINIMUM-tick_elapsed
        print ('minimum wait: %.2f' % wait)
        time.sleep(wait)
    else:
        print ('skip set_timing(); no wait')
    drift = ((time.time() - info['begin']) - info['tick']*TICK -
        TICK_TIMING + info['begin']%TICK)
    print ('drift: %.6f' % drift)

info['tick'] = 0
info['begin'] = time.time()
info['completion_time'] = info['begin'] - TICK

while 1:

    set_timing()

    print('hello world')

    #random real world event
    time.sleep(random.random()*TICK_MINIMUM)

    info['tick'] += 1
    info['completion_time'] = time.time()

En fonction des conditions réelles, vous pouvez obtenir des ticks de longueur:

60,60,62,58,60,60,120,30,30,60,60,60,60,60...etc.

mais au bout de 60 minutes, vous aurez 60 ticks; et la plupart d'entre eux se produiront au décalage correct à la minute que vous préférez.

Sur mon système, la dérive typique est inférieure à 1/20ème de seconde jusqu'à ce que des corrections soient nécessaires.

L'avantage de cette méthode est la résolution de la dérive d'horloge; Cela peut entraîner des problèmes si vous ajoutez par exemple un élément par tick et que vous attendez 60 éléments ajoutés par heure. Si vous ne tenez pas compte de la dérive, des indications secondaires, telles que les moyennes mobiles, peuvent prendre en compte des données trop anciennes, ce qui entraîne une sortie erronée. 

0
litepresence

Voici une version adaptée au code de MestreLion . En plus de la fonction originale, ce code:

1) add first_interval utilisé pour déclencher le chronomètre à une heure précise (l'appelant doit calculer le premier_intervalle et le transmettre)

2) résoudre une condition de concurrence dans le code d'origine. Dans le code d'origine, si le thread de contrôle ne parvenait pas à annuler le temporisateur en cours d'exécution ("Arrêtez le temporisateur et annulez l'exécution de l'action du temporisateur. Ceci ne fonctionnera que si le temporisateur est toujours en attente.", Extrait de https: //docs.python.org/2/library/threading.html ), le temporisateur fonctionnera indéfiniment.

class RepeatedTimer(object):
def __init__(self, first_interval, interval, func, *args, **kwargs):
    self.timer      = None
    self.first_interval = first_interval
    self.interval   = interval
    self.func   = func
    self.args       = args
    self.kwargs     = kwargs
    self.running = False
    self.is_started = False

def first_start(self):
    try:
        # no race-condition here because only control thread will call this method
        # if already started will not start again
        if not self.is_started:
            self.is_started = True
            self.timer = Timer(self.first_interval, self.run)
            self.running = True
            self.timer.start()
    except Exception as e:
        log_print(syslog.LOG_ERR, "timer first_start failed %s %s"%(e.message, traceback.format_exc()))
        raise

def run(self):
    # if not stopped start again
    if self.running:
        self.timer = Timer(self.interval, self.run)
        self.timer.start()
    self.func(*self.args, **self.kwargs)

def stop(self):
    # cancel current timer in case failed it's still OK
    # if already stopped doesn't matter to stop again
    if self.timer:
        self.timer.cancel()
    self.running = False
0
dproc