2010-02-04 5 views
23

De nombreuses tentatives ont été faites dans le passé pour ajouter une fonctionnalité de délai d'attente en Python, de sorte que lorsqu'une limite de temps spécifiée a expiré, le code d'attente peut passer à autre chose. Malheureusement, les recettes précédentes permettaient à la fonction en cours de continuer à fonctionner et de consommer des ressources, ou bien d'arrêter la fonction en utilisant une méthode de terminaison de thread spécifique à la plate-forme. Le but de ce wiki est de développer une réponse multiplateforme à ce problème que de nombreux programmeurs ont dû aborder pour divers projets de programmation.Comment ajouter un délai à une fonction en Python

#! /usr/bin/env python 
"""Provide way to add timeout specifications to arbitrary functions. 

There are many ways to add a timeout to a function, but no solution 
is both cross-platform and capable of terminating the procedure. This 
module use the multiprocessing module to solve both of those problems.""" 

################################################################################ 

__author__ = 'Stephen "Zero" Chappell <[email protected]>' 
__date__ = '11 February 2010' 
__version__ = '$Revision: 3 $' 

################################################################################ 

import inspect 
import sys 
import time 
import multiprocessing 

################################################################################ 

def add_timeout(function, limit=60): 
    """Add a timeout parameter to a function and return it. 

    It is illegal to pass anything other than a function as the first 
    parameter. If the limit is not given, it gets a default value equal 
    to one minute. The function is wrapped and returned to the caller.""" 
    assert inspect.isfunction(function) 
    if limit <= 0: 
     raise ValueError() 
    return _Timeout(function, limit) 

class NotReadyError(Exception): pass 

################################################################################ 

def _target(queue, function, *args, **kwargs): 
    """Run a function with arguments and return output via a queue. 

    This is a helper function for the Process created in _Timeout. It runs 
    the function with positional arguments and keyword arguments and then 
    returns the function's output by way of a queue. If an exception gets 
    raised, it is returned to _Timeout to be raised by the value property.""" 
    try: 
     queue.put((True, function(*args, **kwargs))) 
    except: 
     queue.put((False, sys.exc_info()[1])) 

class _Timeout: 

    """Wrap a function and add a timeout (limit) attribute to it. 

    Instances of this class are automatically generated by the add_timeout 
    function defined above. Wrapping a function allows asynchronous calls 
    to be made and termination of execution after a timeout has passed.""" 

    def __init__(self, function, limit): 
     """Initialize instance in preparation for being called.""" 
     self.__limit = limit 
     self.__function = function 
     self.__timeout = time.clock() 
     self.__process = multiprocessing.Process() 
     self.__queue = multiprocessing.Queue() 

    def __call__(self, *args, **kwargs): 
     """Execute the embedded function object asynchronously. 

     The function given to the constructor is transparently called and 
     requires that "ready" be intermittently polled. If and when it is 
     True, the "value" property may then be checked for returned data.""" 
     self.cancel() 
     self.__queue = multiprocessing.Queue(1) 
     args = (self.__queue, self.__function) + args 
     self.__process = multiprocessing.Process(target=_target, 
               args=args, 
               kwargs=kwargs) 
     self.__process.daemon = True 
     self.__process.start() 
     self.__timeout = self.__limit + time.clock() 

    def cancel(self): 
     """Terminate any possible execution of the embedded function.""" 
     if self.__process.is_alive(): 
      self.__process.terminate() 

    @property 
    def ready(self): 
     """Read-only property indicating status of "value" property.""" 
     if self.__queue.full(): 
      return True 
     elif not self.__queue.empty(): 
      return True 
     elif self.__timeout < time.clock(): 
      self.cancel() 
     else: 
      return False 

    @property 
    def value(self): 
     """Read-only property containing data returned from function.""" 
     if self.ready is True: 
      flag, load = self.__queue.get() 
      if flag: 
       return load 
      raise load 
     raise NotReadyError() 

    def __get_limit(self): 
     return self.__limit 

    def __set_limit(self, value): 
     if value <= 0: 
      raise ValueError() 
     self.__limit = value 

    limit = property(__get_limit, __set_limit, 
        doc="Property for controlling the value of the timeout.") 

Edit: Ce code a été écrit pour Python 3.x et n'a pas été conçu pour les méthodes de classe comme une décoration. Le module multiprocessing n'a pas été conçu pour modifier les instances de classe à travers les limites de processus.

+0

Cette gestion des exceptions ne fonctionne que dans Python 3. Dans 2.x, il jettera la trace de la pile d'origine, affichera l'exception comme provenant de "raise", et l'assertion ne s'affichera pas du tout dans la trace de la pile. –

Répondre

13

Le principal problème avec votre code est la surutilisation de la prévention des conflits d'espaces de noms à double trait de soulignement dans une classe qui n'est pas du tout destinée à être sous-classée.

En général, self.__foo est une odeur de code qui doit être accompagnée d'un commentaire suivant les lignes # This is a mixin and we don't want arbitrary subclasses to have a namespace conflict.

En outre, l'API cliente de cette méthode ressemblerait à ceci:

def mymethod(): pass 

mymethod = add_timeout(mymethod, 15) 

# start the processing  
timeout_obj = mymethod() 
try: 
    # access the property, which is really a function call 
    ret = timeout_obj.value 
except TimeoutError: 
    # handle a timeout here 
    ret = None 

Ce n'est pas très pythonique du tout et une meilleure api client serait:

@timeout(15) 
def mymethod(): pass 

try: 
    my_method() 
except TimeoutError: 
    pass 

Vous utilisez @property dans votre classe pour quelque chose qui est un accesseur mutant d'état, ce n'est pas une bonne idée. Par exemple, que se passerait-il lorsque .value est accédé deux fois? Il semble que cela échouerait car queue.get() renverrait la corbeille car la file d'attente est déjà vide.

Supprime entièrement @property. Ne l'utilisez pas dans ce contexte, il ne convient pas à votre cas d'utilisation. Faites appelez lorsque appelé et renvoyez la valeur ou augmentez l'exception elle-même. Si vous devez vraiment avoir la valeur accessible plus tard, faites-en une méthode comme .get() ou .value().

Ce code pour le _target devrait être reformulé un peu:

def _target(queue, function, *args, **kwargs): 
    try: 
     queue.put((True, function(*args, **kwargs))) 
    except: 
     queue.put((False, exc_info())) # get *all* the exec info, don't do exc_info[1] 

# then later: 
    raise exc_info[0], exc_info[1], exc_info[2] 

De cette façon, la trace de la pile sera conservée correctement et visible au programmeur.

Je pense que vous avez fait une première tentative raisonnable d'écrire une bibliothèque utile, j'aime l'utilisation du module de traitement pour atteindre les objectifs.

+0

Le double trait de soulignement n'est-il pas le seul moyen d'approcher la création d'une variable privée en Python? Les variables privées sont préférées dans la programmation orientée objet réelle car c'est ainsi que fonctionne l'encapsulation, ouais? – BillR

+0

@BillR: Python n'a pas de "vraies" variables privées. Excepté le nom manquant des noms de classe à double soulignement-préfixé en dehors de la classe, rien d'autre n'est fait pour les imposer étant privé et vous pouvez facilement contourner le problème si vous savez comment cela fonctionne. En dépit de tout cela, il est tout à fait possible d'écrire du code orienté objet en l'utilisant, donc l'application de l'encapsulation n'est pas une condition préalable dans un langage de programmation. – martineau

6

Voici comment obtenir la syntaxe de décorateur Jerub mentionné

def timeout(limit=None): 
    if limit is None: 
     limit = DEFAULT_TIMEOUT 
    if limit <= 0: 
     raise TimeoutError() # why not ValueError here? 
    def wrap(function): 
     return _Timeout(function,limit) 
    return wrap 

@timeout(15) 
def mymethod(): pass 
+0

J'ai déjà utilisé la syntaxe du décorateur mais je ne le recommanderais pas dans ce cas. –

+0

@NoctisSkytower pourquoi ne recommanderiez-vous pas un décorateur dans ce cas? Selon vous, quel est le désavantage ou le risque? –

+0

@tristan: Le code le plus décoré implique des méthodes dans les classes. En fonction de la façon dont le multitraitement fonctionne dans cet exemple, les modifications apportées au code décoré ne sont pas reflétées dans l'objet d'origine. Toutes les modifications restent dans le second processus que la fonction 'add_timeout' finit par créer. –

1

La bibliothèque Pebble a été conçu pour offrir la mise en œuvre multi-plateforme capable de faire face à la logique problématique qui pourrait crash, segfault or run indefinitely.

from pebble import concurrent 

@concurrent.process(timeout=10) 
def function(foo, bar=0): 
    return foo + bar 

future = function(1, bar=2) 

try: 
    result = future.result() # blocks until results are ready 
except Exception as error: 
    print("Function raised %s" % error) 
    print(error.traceback) # traceback of the function 
except TimeoutError as error: 
    print("Function took longer than %d seconds" % error.args[1]) 

Le décorateur fonctionne aussi avec des méthodes statiques et de classe. Je ne recommanderais pas pour décorer les méthodes néanmoins, car c'est une pratique assez sujettes aux erreurs.

Questions connexes