web-dev-qa-db-fra.com

Attraper une exception de fil dans le fil de l'appelant en Python

Je suis très novice en Python et en programmation multithread en général. En gros, j'ai un script qui va copier les fichiers dans un autre endroit. Je voudrais que cela soit placé dans un autre thread afin que je puisse sortir .... pour indiquer que le script est toujours en cours d'exécution.

Le problème que je rencontre est que si les fichiers ne peuvent pas être copiés, une exception sera levée. C'est ok si vous exécutez dans le thread principal; Cependant, le code suivant ne fonctionne pas:

try:
    threadClass = TheThread(param1, param2, etc.)
    threadClass.start()   ##### **Exception takes place here**
except:
    print "Caught an exception"

Dans la classe de thread elle-même, j'ai essayé de relancer l'exception, mais cela ne fonctionne pas. J'ai vu des gens ici poser des questions similaires, mais ils semblent tous faire quelque chose de plus spécifique que ce que j'essaie de faire (et je ne comprends pas très bien les solutions proposées). J'ai vu des gens mentionner l'utilisation de sys.exc_info(), mais je ne sais pas où et comment l'utiliser.

Toute aide est grandement appréciée!

EDIT: Le code de la classe de thread est ci-dessous:

class TheThread(threading.Thread):
    def __init__(self, sourceFolder, destFolder):
        threading.Thread.__init__(self)
        self.sourceFolder = sourceFolder
        self.destFolder = destFolder

    def run(self):
        try:
           shul.copytree(self.sourceFolder, self.destFolder)
        except:
           raise
154
Phanto

Le problème est que thread_obj.start() retourne immédiatement. Le thread enfant que vous avez créé s'exécute dans son propre contexte, avec sa propre pile. Toute exception qui s'y produit se trouve dans le contexte du thread enfant et se trouve dans sa propre pile. Une façon dont je peux penser en ce moment pour communiquer ces informations au thread parent consiste à utiliser une sorte de message de transmission, afin que vous puissiez examiner cela.

Essayez ceci pour la taille:

import sys
import threading
import Queue


class ExcThread(threading.Thread):

    def __init__(self, bucket):
        threading.Thread.__init__(self)
        self.bucket = bucket

    def run(self):
        try:
            raise Exception('An error occured here.')
        except Exception:
            self.bucket.put(sys.exc_info())


def main():
    bucket = Queue.Queue()
    thread_obj = ExcThread(bucket)
    thread_obj.start()

    while True:
        try:
            exc = bucket.get(block=False)
        except Queue.Empty:
            pass
        else:
            exc_type, exc_obj, exc_trace = exc
            # deal with the exception
            print exc_type, exc_obj
            print exc_trace

        thread_obj.join(0.1)
        if thread_obj.isAlive():
            continue
        else:
            break


if __== '__main__':
    main()
95
Santa

Le module concurrent.futures simplifie le travail dans des threads (ou processus) distincts et permet de gérer les exceptions résultantes:

import concurrent.futures
import shutil

def copytree_with_dots(src_path, dst_path):
    with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
        # Execute the copy on a separate thread,
        # creating a future object to track progress.
        future = executor.submit(shutil.copytree, src_path, dst_path)

        while future.running():
            # Print pretty dots here.
            pass

        # Return the value returned by shutil.copytree(), None.
        # Raise any exceptions raised during the copy process.
        return future.result()

concurrent.futures est inclus dans Python 3.2 et est disponible sous la forme/ le module futures backported pour les versions antérieures.

28
Jon-Eric

Bien qu'il ne soit pas possible d'intercepter directement une exception générée dans un autre thread, voici un code permettant d'obtenir de manière assez transparente quelque chose de très proche de cette fonctionnalité. Votre thread enfant doit sous-classer la classe ExThread au lieu de threading.Thread et le thread parent doit appeler la méthode child_thread.join_with_exception() au lieu de child_thread.join() lorsqu'il attend que le thread termine son travail.

Détails techniques de cette implémentation: lorsque le thread enfant lève une exception, celle-ci est transmise au parent par le biais d'une Queue, puis renvoyée dans le thread parent. Notez qu'il n'y a pas d'attente occupée dans cette approche.

#!/usr/bin/env python

import sys
import threading
import Queue

class ExThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.__status_queue = Queue.Queue()

    def run_with_exception(self):
        """This method should be overriden."""
        raise NotImplementedError

    def run(self):
        """This method should NOT be overriden."""
        try:
            self.run_with_exception()
        except BaseException:
            self.__status_queue.put(sys.exc_info())
        self.__status_queue.put(None)

    def wait_for_exc_info(self):
        return self.__status_queue.get()

    def join_with_exception(self):
        ex_info = self.wait_for_exc_info()
        if ex_info is None:
            return
        else:
            raise ex_info[1]

class MyException(Exception):
    pass

class MyThread(ExThread):
    def __init__(self):
        ExThread.__init__(self)

    def run_with_exception(self):
        thread_name = threading.current_thread().name
        raise MyException("An error in thread '{}'.".format(thread_name))

def main():
    t = MyThread()
    t.start()
    try:
        t.join_with_exception()
    except MyException as ex:
        thread_name = threading.current_thread().name
        print "Caught a MyException in thread '{}': {}".format(thread_name, ex)

if __== '__main__':
    main()
27
Mateusz Kobos

Si une exception se produit dans un thread, le meilleur moyen consiste à le relancer dans le thread de l'appelant au cours de join. Vous pouvez obtenir des informations sur l'exception en cours de traitement à l'aide de la fonction sys.exc_info(). Ces informations peuvent simplement être stockées en tant que propriété de l'objet thread jusqu'à ce que join soit appelé. À ce stade, il peut être à nouveau généré.

Notez qu'un Queue.Queue (comme suggéré dans d'autres réponses) n'est pas nécessaire dans ce cas simple où le thread lève au plus 1 exception et se termine juste après le lancement d'une exception. Nous évitons les conditions de concurrence en attendant simplement que le fil soit terminé.

Par exemple, étendez ExcThread (ci-dessous) en remplaçant excRun (au lieu de run).

Python 2.x:

import threading

class ExcThread(threading.Thread):
  def excRun(self):
    pass

  def run(self):
    self.exc = None
    try:
      # Possibly throws an exception
      self.excRun()
    except:
      import sys
      self.exc = sys.exc_info()
      # Save details of the exception thrown but don't rethrow,
      # just complete the function

  def join(self):
    threading.Thread.join(self)
    if self.exc:
      msg = "Thread '%s' threw an exception: %s" % (self.getName(), self.exc[1])
      new_exc = Exception(msg)
      raise new_exc.__class__, new_exc, self.exc[2]

Python 3.x:

La forme à 3 arguments pour raise a disparu de Python 3, remplacez la dernière ligne par:

raise new_exc.with_traceback(self.exc[2])
15
Rok Strniša

Il y a beaucoup de réponses vraiment étrangement compliquées à cette question. Est-ce que je simplifie trop, parce que cela me semble suffisant pour la plupart des choses.

from threading import Thread

class PropagatingThread(Thread):
    def run(self):
        self.exc = None
        try:
            if hasattr(self, '_Thread__target'):
                # Thread uses name mangling prior to Python 3.
                self.ret = self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
            else:
                self.ret = self._target(*self._args, **self._kwargs)
        except BaseException as e:
            self.exc = e

    def join(self):
        super(PropagatingThread, self).join()
        if self.exc:
            raise self.exc
        return self.ret

Si vous êtes certain de ne jamais utiliser que l'une ou l'autre des versions de Python, vous pouvez réduire la méthode run() à la version endommagée (si vous n'exécutez que des versions de Python antérieures à la version 3), ou juste la version propre (si vous ne vous exécuterez que sur les versions de Python commençant par 3).

Exemple d'utilisation:

def f(*args, **kwargs)
    print(args)
    print(kwargs)
    raise Exception('I suck')

t = PropagatingThread(target=f, args=(5,), kwargs={'hello':'world'})
t.start()
t.join()

Et vous verrez l'exception levée sur l'autre thread lorsque vous rejoindrez.

11
ArtOfWarfare

C’était un vilain petit problème, et j’aimerais ajouter ma solution. Certaines autres solutions que j’ai trouvées (async.io par exemple) semblaient prometteuses mais présentaient aussi un peu une boîte noire. L'approche de la boucle file d'attente/événement vous lie à une certaine implémentation. Le code source des contrats à terme simultanés ne représente toutefois qu’environ 1000 lignes et est facile à comprendre . Cela me permettait de résoudre facilement mon problème: créer des threads de travail ad-hoc sans trop de configuration et pouvoir intercepter des exceptions dans le thread principal.

Ma solution utilise les API simultanée Futures et API de threading. Il vous permet de créer un ouvrier qui vous donne à la fois le fil et le futur. De cette façon, vous pouvez rejoindre le fil pour attendre le résultat:

worker = Worker(test)
thread = worker.start()
thread.join()
print(worker.future.result())

... ou vous pouvez laisser le travailleur envoyer un rappel lorsque vous avez terminé:

worker = Worker(test)
thread = worker.start(lambda x: print('callback', x))

... ou vous pouvez boucler jusqu'à la fin de l'événement:

worker = Worker(test)
thread = worker.start()

while True:
    print("waiting")
    if worker.future.done():
        exc = worker.future.exception()
        print('exception?', exc)
        result = worker.future.result()
        print('result', result)           
        break
    time.sleep(0.25)

Voici le code:

from concurrent.futures import Future
import threading
import time

class Worker(object):
    def __init__(self, fn, args=()):
        self.future = Future()
        self._fn = fn
        self._args = args

    def start(self, cb=None):
        self._cb = cb
        self.future.set_running_or_notify_cancel()
        thread = threading.Thread(target=self.run, args=())
        thread.daemon = True #this will continue thread execution after the main thread runs out of code - you can still ctrl + c or kill the process
        thread.start()
        return thread

    def run(self):
        try:
            self.future.set_result(self._fn(*self._args))
        except BaseException as e:
            self.future.set_exception(e)

        if(self._cb):
            self._cb(self.future.result())

... et la fonction de test:

def test(*args):
    print('args are', args)
    time.sleep(2)
    raise Exception('foo')
4
Calvin Froedge

concurrent.futures.as_completed

https://docs.python.org/3.7/library/concurrent.futures.html#concurrent.futures.as_completed

La solution suivante:

  • retourne au thread principal immédiatement quand une exception est appelée
  • ne nécessite aucune classe supplémentaire définie par l'utilisateur car il n'a pas besoin de:
    • une Queue explicite
    • ajouter une exception autour de votre fil de travail 

La source:

#!/usr/bin/env python3

import concurrent.futures
import time

def func_that_raises(do_raise):
    for i in range(3):
        print(i)
        time.sleep(0.1)
    if do_raise:
        raise Exception()
    for i in range(3):
        print(i)
        time.sleep(0.1)

with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    futures = []
    futures.append(executor.submit(func_that_raises, False))
    futures.append(executor.submit(func_that_raises, True))
    for future in concurrent.futures.as_completed(futures):
        print(repr(future.exception()))

Sortie possible:

0
0
1
1
2
2
0
Exception()
1
2
None

Il n'est malheureusement pas possible de tuer des contrats à terme pour annuler les autres en cas d'échec:

Si vous faites quelque chose comme:

for future in concurrent.futures.as_completed(futures):
    if future.exception() is not None:
        raise future.exception()

alors la with l'attrape et attend que le deuxième thread se termine avant de continuer. Le comportement suivant est similaire:

for future in concurrent.futures.as_completed(futures):
    future.result()

depuis future.result(), une exception est déclenchée à nouveau.

Si vous souhaitez quitter tout le processus Python, vous pouvez vous en tirer avec os._exit(0) , mais cela signifie probablement que vous avez besoin d'un refactor.

Testé sur Python 3.6.7, Ubuntu 18.04.

En tant que novice de Threading, il m'a fallu beaucoup de temps pour comprendre comment implémenter le code de Mateusz Kobos (ci-dessus). Voici une version clarifiée pour aider à comprendre comment l’utiliser. 

#!/usr/bin/env python

import sys
import threading
import Queue

class ExThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.__status_queue = Queue.Queue()

    def run_with_exception(self):
        """This method should be overriden."""
        raise NotImplementedError

    def run(self):
        """This method should NOT be overriden."""
        try:
            self.run_with_exception()
        except Exception:
            self.__status_queue.put(sys.exc_info())
        self.__status_queue.put(None)

    def wait_for_exc_info(self):
        return self.__status_queue.get()

    def join_with_exception(self):
        ex_info = self.wait_for_exc_info()
        if ex_info is None:
            return
        else:
            raise ex_info[1]

class MyException(Exception):
    pass

class MyThread(ExThread):
    def __init__(self):
        ExThread.__init__(self)

    # This overrides the "run_with_exception" from class "ExThread"
    # Note, this is where the actual thread to be run lives. The thread
    # to be run could also call a method or be passed in as an object
    def run_with_exception(self):
        # Code will function until the int
        print "sleeping 5 seconds"
        import time
        for i in 1, 2, 3, 4, 5:
            print i
            time.sleep(1) 
        # Thread should break here
        int("str")
# I'm honestly not sure why these appear here? So, I removed them. 
# Perhaps Mateusz can clarify?        
#         thread_name = threading.current_thread().name
#         raise MyException("An error in thread '{}'.".format(thread_name))

if __== '__main__':
    # The code lives in MyThread in this example. So creating the MyThread 
    # object set the code to be run (but does not start it yet)
    t = MyThread()
    # This actually starts the thread
    t.start()
    print
    print ("Notice 't.start()' is considered to have completed, although" 
           " the countdown continues in its new thread. So you code "
           "can tinue into new processing.")
    # Now that the thread is running, the join allows for monitoring of it
    try:
        t.join_with_exception()
    # should be able to be replace "Exception" with specific error (untested)
    except Exception, e: 
        print
        print "Exceptioon was caught and control passed back to the main thread"
        print "Do some handling here...or raise a custom exception "
        thread_name = threading.current_thread().name
        e = ("Caught a MyException in thread: '" + 
             str(thread_name) + 
             "' [" + str(e) + "]")
        raise Exception(e) # Or custom class of exception, such as MyException
2
RightmireM

De la même manière que RickardSjogren sans Queue, sys, etc., mais aussi sans quelques écouteurs de signaux: exécuter directement un gestionnaire d'exceptions qui correspond à un bloc except.

#!/usr/bin/env python3

import threading

class ExceptionThread(threading.Thread):

    def __init__(self, callback=None, *args, **kwargs):
        """
        Redirect exceptions of thread to an exception handler.

        :param callback: function to handle occured exception
        :type callback: function(thread, exception)
        :param args: arguments for threading.Thread()
        :type args: Tuple
        :param kwargs: keyword arguments for threading.Thread()
        :type kwargs: dict
        """
        self._callback = callback
        super().__init__(*args, **kwargs)

    def run(self):
        try:
            if self._target:
                self._target(*self._args, **self._kwargs)
        except BaseException as e:
            if self._callback is None:
                raise e
            else:
                self._callback(self, e)
        finally:
            # Avoid a refcycle if the thread is running a function with
            # an argument that has a member that points to the thread.
            del self._target, self._args, self._kwargs, self._callback

Seul self._callback et le bloc except de run () s'ajoutent à threading.Thread normal.

1
Chickenmarkus

L'utilisation d'exceptions nues n'est pas une bonne pratique car vous attrapez généralement plus que ce que vous négociez.

Je suggérerais de modifier la except pour ne saisir que l'exception que vous voudriez gérer. Je ne pense pas que le relancer produise l'effet escompté, car si vous instanciez TheThread dans le try extérieur, s'il y a une exception, l'assignation ne se produira jamais.

Au lieu de cela, vous voudrez peut-être simplement alerter et passer à autre chose, par exemple:

def run(self):
    try:
       shul.copytree(self.sourceFolder, self.destFolder)
    except OSError, err:
       print err

Ensuite, lorsque cette exception est interceptée, vous pouvez la gérer ici. Ensuite, lorsque la variable try extérieure intercepte une exception de TheThread, vous savez que ce ne sera pas celle que vous avez déjà gérée et vous aidera à isoler votre flux de processus.

0
jathanism

Une méthode que j'aime beaucoup est basée sur le motif observer . Je définis une classe de signal que mon thread utilise pour émettre des exceptions aux auditeurs. Il peut également être utilisé pour renvoyer des valeurs de threads. Exemple:

import threading

class Signal:
    def __init__(self):
        self._subscribers = list()

    def emit(self, *args, **kwargs):
        for func in self._subscribers:
            func(*args, **kwargs)

    def connect(self, func):
        self._subscribers.append(func)

    def disconnect(self, func):
        try:
            self._subscribers.remove(func)
        except ValueError:
            raise ValueError('Function {0} not removed from {1}'.format(func, self))


class WorkerThread(threading.Thread):

    def __init__(self, *args, **kwargs):
        super(WorkerThread, self).__init__(*args, **kwargs)
        self.Exception = Signal()
        self.Result = Signal()

    def run(self):
        if self._Thread__target is not None:
            try:
                self._return_value = self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
            except Exception as e:
                self.Exception.emit(e)
            else:
                self.Result.emit(self._return_value)

if __== '__main__':
    import time

    def handle_exception(exc):
        print exc.message

    def handle_result(res):
        print res

    def a():
        time.sleep(1)
        raise IOError('a failed')

    def b():
        time.sleep(2)
        return 'b returns'

    t = WorkerThread(target=a)
    t2 = WorkerThread(target=b)
    t.Exception.connect(handle_exception)
    t2.Result.connect(handle_result)
    t.start()
    t2.start()

    print 'Threads started'

    t.join()
    t2.join()
    print 'Done'

Je n'ai pas suffisamment d'expérience de travail avec les threads pour prétendre qu'il s'agit d'une méthode totalement sûre. Mais cela a fonctionné pour moi et j'aime la flexibilité.

0
RickardSjogren

Je sais que je suis un peu en retard pour la fête ici, mais je rencontrais un problème très similaire, mais cela impliquait d'utiliser tkinter en tant qu'interface graphique, et mainloop rendait impossible l'utilisation de solutions dépendantes de .join (). J'ai donc adapté la solution donnée dans l'EDIT de la question initiale, mais en la rendant plus générale pour la rendre plus compréhensible pour les autres.

Voici la nouvelle classe de thread en action:

import threading
import traceback
import logging


class ExceptionThread(threading.Thread):
    def __init__(self, *args, **kwargs):
        threading.Thread.__init__(self, *args, **kwargs)

    def run(self):
        try:
            if self._target:
                self._target(*self._args, **self._kwargs)
        except Exception:
            logging.error(traceback.format_exc())


def test_function_1(input):
    raise IndexError(input)


if __== "__main__":
    input = 'useful'

    t1 = ExceptionThread(target=test_function_1, args=[input])
    t1.start()

Bien sûr, vous pouvez toujours lui demander de gérer l'exception d'une autre manière, par exemple en l'imprimant ou en l'envoyant vers la console.

Cela vous permet d'utiliser la classe ExceptionThread exactement comme vous le feriez avec la classe Thread, sans aucune modification particulière. 

0
Firo

Enroulez le fil avec un stockage d'exception.

import threading
import sys
class ExcThread(threading.Thread):

    def __init__(self, target, args = None):
        self.args = args if args else []
        self.target = target
        self.exc = None
        threading.Thread.__init__(self)

    def run(self):
        try:
            self.target(*self.args)
            raise Exception('An error occured here.')
        except Exception:
            self.exc=sys.exc_info()

def main():
    def hello(name):
        print(!"Hello, {name}!")
    thread_obj = ExcThread(target=hello, args=("Jack"))
    thread_obj.start()

    thread_obj.join()
    exc = thread_obj.exc
    if exc:
        exc_type, exc_obj, exc_trace = exc
        print(exc_type, ':',exc_obj, ":", exc_trace)

main()
0
ahuigo

Un moyen simple d'attraper l'exception du thread et de communiquer avec la méthode de l'appelant peut être de passer un dictionnaire ou une liste à la méthode worker.

Exemple (passage du dictionnaire à la méthode worker): 

import threading

def my_method(throw_me):
    raise Exception(throw_me)

def worker(shared_obj, *args, **kwargs):
    try:
        shared_obj['target'](*args, **kwargs)
    except Exception as err:
        shared_obj['err'] = err

shared_obj = {'err':'', 'target': my_method}
throw_me = "Test"

th = threading.Thread(target=worker, args=(shared_obj, throw_me), kwargs={})
th.start()
th.join()

if shared_obj['err']:
    print(">>%s" % shared_obj['err'])
0
rado stoyanov