2010-02-04 11 views
23

In der Vergangenheit wurden viele Versuche unternommen, die Zeitüberschreitungsfunktion in Python so zu erweitern, dass nach Ablauf einer bestimmten Zeit der Wartecode weiterlaufen konnte. Leider erlaubten frühere Rezepte entweder, dass die running-Funktion weiter ausgeführt wurde und Ressourcen verbrauchte, oder sie töteten die Funktion mithilfe einer plattformspezifischen Methode der Thread-Beendigung. Der Zweck dieses Wikis ist es, eine plattformübergreifende Antwort auf dieses Problem zu entwickeln, die viele Programmierer für verschiedene Programmierprojekte zu bewältigen hatten.So fügen Sie einer Funktion in Python eine Zeitüberschreitung hinzu

#! /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: Dieser Code wurde für Python 3.x geschrieben und wurde nicht für Klassenmethoden als Dekoration entworfen. Das multiprocessing Modul wurde nicht entworfen, um Klasseninstanzen über die Prozessgrenzen hinweg zu ändern.

+0

Diese Ausnahmebehandlung funktioniert nur in Python 3. In 2.x, es wird den ursprünglichen Stack-Trace wegwerfen, die Exception als Ursprung auf dem "Raise" anzeigen, und das Assert wird überhaupt nicht im Stack-Trace angezeigt. –

Antwort

13

Das Hauptproblem mit Ihrem Code ist die übermäßige Verwendung des Konflikts in der doppelten Unterstrich-Namespace-Konfliktprävention in einer Klasse, die überhaupt nicht unterklassifiziert werden soll.

Im Allgemeinen ist self.__foo ein Code-Geruch, der von einem Kommentar entlang der Linien # This is a mixin and we don't want arbitrary subclasses to have a namespace conflict begleitet werden sollte.

Weiterhin ist der Client-API dieser Methode würde wie folgt aussehen:

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 

Das ist gar nicht sehr pythonic und eine bessere Client api wäre:

@timeout(15) 
def mymethod(): pass 

try: 
    my_method() 
except TimeoutError: 
    pass 

Sie @property verwenden in Ihrer Klasse für etwas, das ein Zustands mutierender Accessor ist, ist dies keine gute Idee. Was passiert beispielsweise, wenn auf .value zweimal zugegriffen wird? Es sieht so aus, als würde es fehlschlagen, weil queue.get() den Papierkorb zurückgibt, weil die Warteschlange bereits leer ist.

Entfernen Sie @property vollständig. Verwenden Sie es nicht in diesem Zusammenhang, es ist nicht für Ihren Anwendungsfall geeignet. Machen Sie Aufruf Block beim Aufruf und den Wert zurückgeben oder die Ausnahme selbst auslösen. Wenn Sie wirklich Wert später zugegriffen haben müssen, stellen Sie eine Methode wie .get() oder .value().

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] 

Auf diese Weise wird der Stack-Trace richtig und sichtbar für den Programmierer erhalten werden:

Dieser Code für die _target sollte ein wenig neu geschrieben werden.

Ich denke, Sie haben einen vernünftigen ersten Riss beim Schreiben einer nützlichen Bibliothek gemacht, ich mag die Verwendung des Verarbeitungsmoduls, um die Ziele zu erreichen.

+0

Ist der doppelte Unterstrich nicht die einzige Möglichkeit, eine private Variable in Python zu erstellen? Private Variablen werden in der realen objektorientierten Programmierung bevorzugt, da so die Kapselung funktioniert, ja? – BillR

+0

@BillR: Python hat keine "echten" privaten Variablen. Abgesehen von dem Namen Mangling von Klassennamen mit doppeltem Unterstrich außerhalb der Klasse wird nichts anderes getan, um sie zu zwingen, privat zu sein - und Sie können es leicht umgehen, wenn Sie wissen, wie es funktioniert. Trotz alledem ist es möglich, objektorientierten Code mit ihm zu schreiben, also ist die erzwungene Kapselung keine Voraussetzung dafür in irgendeiner Programmiersprache. – martineau

6

Dies ist, wie die Dekorateur Syntax Jerub

wurde
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

Ich habe die Decorator-Syntax vorher verwendet, würde sie aber in diesem Fall nicht empfehlen. –

+0

@NoctisSkytower Warum würden Sie in diesem Fall keinen Dekorateur empfehlen? Was ist Ihrer Meinung nach der Nachteil oder das Risiko? –

+0

@tristan: Der am meisten dekorierte Code beinhaltet Methoden in Klassen. Abhängig davon, wie Multiprozessing in diesem Beispiel funktioniert, werden alle Änderungen, die im dekorierten Code vorgenommen werden, nicht im ursprünglichen Objekt wiedergegeben. Alle Änderungen bleiben im zweiten Prozess, der mit der Funktion 'add_timeout' endet. –

1

Die Pebble Bibliothek erwähnt bekommen entworfen von mit problematischer Logik zu tun fähig Cross-Plattform-Implementierung zu bieten, die crash, segfault or run indefinitely kann.

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

Der Dekorateur funktioniert auch mit statischen und Klassenmethoden. Ich würde nicht empfehlen, Methoden trotzdem zu dekorieren, da es eine ziemlich fehleranfällige Praxis ist.

Verwandte Themen