Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

raise : comment gérer le nettoyage dans la fonction qui lève l’exception ?

+1 vote

Au moment de lever une exception, comment peut on proprement invoquer le code de nettoyage des données de la méthode ? Par exemple :

# On lance un thread qui bloque sur le fd d'une socket, et 
# remplit ReadThread.data_received avec les donnees lues.
read_thread = ReadThread(socket_fd)
read_thread.start()

# Si on n'a pas reçu les trames de demarrage au bout de 5 secondes
sleep(5)
if read_thread.data_received != "StartMessage":
    raise StartDatasException

while True:
    read_thread.data_received = None
    sleep(5)
    # Si on n'a pas reçu le heartbeat au bout de 5 secondes
    if read_thread.data_received != "Heartbeat":
        raise HeartBeatException

Comment gérer l’arrêt du thread, la fermeture de la socket… de maniere unifiee pour les 2 raise d’exception ?

demandé 6-Jan-2016 par toub (430 points)

de maniere unifiee pour les 2 raise d’exception

Pourquoi voudrais-tu les gérer de manière unifier? il peut être pertinent de lever une exception particulière au thread, et une autre à la fermeture du socket non? cela aiderait un peu plus au débug de fragmenter ton problème en deux sous-problèmes .

@boblinux
ce que je voulais dire dans cet exemple, c'est que lorsque je lève l'une ou l'autre de ces 2 exceptions (je sais qu'elles se ressemblent beaucoup mais c'est juste un exemple, j'ai rien trouvé de mieux) je souhaite faire un nettoyage de mes données (ici, arrêt du thread, fermeture socket, ...). C'est dans ce sens que je parle de traitement unifié, pas dans le sens de lever une seule classe d'exception.

4 Réponses

+3 votes
 
Meilleure réponse

Ça n'existe pas en python, et je ne pense pas que ce soit une bonne façon de faire. On ne nettoie pas quoi que ce soit avant de lancer une exception, mais bien après (dans le cas où on décide de la traiter).

S'il y a des trucs à nettoyer, ça doit être géré dans le bloc de gestion d'exception, à l'extérieur donc, typiquement avec except ou finally. D'ailleurs, il existe une notion de context manager qui va gérer le finally de façon transparente dans le code.

répondu 6-Jan-2016 par yoch (2,514 points)
sélectionné 8-Jan-2016 par toub

Le problème, c'est que dans le except ou finally, on ne peut nettoyer que les données auxquelles on a accès; on n'a pas forcément connaissances des données du niveau ayant levé l'exception. Dès lors, comment les nettoyer correctement ?

Généralement, si, on y a accès. C'est d'ailleurs le cas dans ton code, le thread principal a créé la socket et lancé le thread, il y a donc accès. On sait aussi ce qu'on doit nettoyer, si le mécanisme d'exception est bien employé. (d'ailleurs, tu peux utiliser l'exception elle même pour remonter les informations qui manqueraient, mais ce n'est pas le genre de truc dont il faut abuser)

Lorsque tu n'as pas accès à la ressource que tu dois nettoyer à l'endroit où tu pose le try, il vaut mieux utiliser un context manager au sein du code interne (donc en fait un deuxième gestionnaire d'exception caché) pour gérer ce point.

Il ne faut pas hésiter à gérer ses exceptions sur plusieurs niveaux si nécessaire (dans ce cas, on fait ce qu'il faut au niveau n, puis on raise vers le niveau n-1).

Je vais regarder le coup du context manager. Je retiens également le fait de traiter les exceptions sur plusieurs niveaux. Merci pour toutes ces réponses

+3 votes

La première solution que je vois est quelque chose comme ça :

try:
    # du code qui lève HeartBeatException ou StartDatasException
except (HeartBeatException, StartDatasException):
    # du code qui gère les exceptions
except (BrainCrashException, StopCodesException) as e:
    # du code qui gère deux autres exceptions, et qui peux même y accéder via la variable e

Une autre solution serait de définir, plutôt que deux exceptions utilisées en deux endroits mais qui veulent dire la même chose (et mène au même traitement en aval), une seule exception qui serait levée en deux endroit. (et qui aurait un nom plus généraliste, et peut-être même qu'une exception built-in suffirait)

répondu 6-Jan-2016 par lucas (2,340 points)

Ma question porte sur le code qui lève l'exception, pas sur celui qui catche.
Si je reformule, c'est : existe-t-il une syntaxe python permettant de faire

def cleanup():
    pass

if error1_detected:
    cleanup()
    raise Exception1

if error2_detected:
    cleanup()
    raise Exception2

mais de façon à ce que le cleanup soit invoqué de manière plus systématique, genre :

raise.before_raise=cleanup

if error1_detected:
    #Invocation automatique de raise.before_raise en amont du raise
    raise Exception1

if error2_detected:
    #Invocation automatique de raise.before_raise en amont du raise
    raise Exception2

C'est, je crois, exactement ce que fait le code suivant :

def cleanup():
    # do something
try:
    # code pouvant lever error1 et error2
except error1:
    cleanup()
    # code traitant l'erreur 1
except error2:
    cleanup()
    # code traitant l'erreur 2

Ce qui peut éventuellement être amélioré en passant l'erreur interceptée en argument de la fonction cleanup.

+1 vote

Comme dit @yoch.
On nettoie après que l'exception soit levée. Ou dit autrement, on nettoie un (ou plus) niveau au dessus.

Pour cela, il suffit de séparer la création du thread et le process. Si il n'y a pas de traitement particulier à l'erreur, ça donne :

thread = ReadThread(socket_fd)
try:
    # peut emettre StartDatasException, HeartBeatException
    # ou même n'importe quoi d'autre
    process(thread)
finally:
    # toujours appelé
    cleanup(thread)

Si il y a une action particulière liée à certaines erreurs :

thread = ReadThread(socket_fd)
try:
    process(thread)
except StartDatasException, HeartBeatException:
    special_cleanup(thread)
    raise  # relance l'exception
finally:
    cleanup(thread)

Ici le programme va s'arrêter si StartDatasException ou HeartBeatException est levée. Il suffit d'enlever le 'raise' pour que le programme puisse continuer.

répondu 7-Jan-2016 par bubulle (2,256 points)

Dans le cas ou l'exception a été levé par le niveau juste en dessous, OK.
Mais dans d'autres cas, l'exception vient de couches beaucoup plus basses. Au niveau du except, on ne connaît à priori pas les données à nettoyer.

+3 votes

D'abord, notons qu'en python, on lira presque jamais directement soit-même depuis une socket. L'immense majorité des protocoles courants ont une lib qui en fait l'abstraction, et il a vraiment très peu de raison de créer son propre protocole.

Ensuite, en manipulant un thread, on en ferait pas l'attache du résultat à un objet "readthread.datareceived" car avoir un état partagé, c'est le risque d'avoir des races conditions.

Mais on ne va pas corriger tout ça, ce n'est pas le but, donc restons sur l'exemple donné.

Le problème ici est qu'il manque un code : tu as le code de gestion de l'IO, et tu as le code de gestion de ton protocole réseau. Ils doivent être séparés et encapsulés, puis ensuite être utilisés par un 3eme code: le controleur de ton application.

En effet si tu lèves toi-même une exception, c'est que tu t'attends à ce qu'un autre code utilise le tient. Il n'y a aucun sens de lever une exception juste pour soit.

Ca tombe bien, toute bonne application toujours un code central qui coordonne tous ses composants. Le code de ce code, le controlleur, est au minimum de prendre la config, lancer les composants, et faire des prints.

Bref, ici il faut voir (je garde la mauvaise archi, on ne fait bien entendu par quelque chose d'aussi brut):

def read():
    read_thread = ReadThread(socket_fd)
    read_thread.start()


def process():

    def welcome(self):
        # Si on n'a pas reçu les trames de demarrage au bout de 5 secondes
        sleep(5)
        if read_thread.data_received != "StartMessage":
            raise StartDatasException

    def keep_alive(self):
        while True:
            read_thread.data_received = None
            sleep(5)
            # Si on n'a pas reçu le heartbeat au bout de 5 secondes
            if read_thread.data_received != "Heartbeat":
                raise HeartBeatException

# controller
def main():
    socket_fd = open_socket()
    read()
    try:
        process()
    except StartDatasException:
        # faire un truc ici en cas d'erreur : log, affichage, etc
    except HeartBeatException:
        # faire un truc ici en cas d'erreur : log, affichage, etc
    finally:
        # mettre le code de nettoyage ici en s'assurant que les ressources
        # ne sont pas encore lus (donc lock, etc)

Après on peut améliorer ça en utilisant un contexte manager (with) qui lance le finally automatiquement.

Néanmoins, comme je le répète, on organiserait jamais un code Python lisant les sockets ainsi. On utiliserait soit Twisted/Asyncio, soit, quitte à le faire à la main, on ferait des workers qui communiquent entre eux à base de Queue. Une fois chaque worker isolé, le debuggage est beaucoup plus propre puisque les exceptions sont limités à chaque worker.

répondu 7-Jan-2016 par Sam (5,000 points)

Je me rends bien compte à travers les commentaires que je garde un raisonnement C/C++, que je dois désapprendre pour le mode python.
Je retiens ta suggestion d'utilisation de Twisted/Asyncio ou de workers/queue (t'as pas pondu un article la dessus par hasard ? :-) et mon problème de nettoyage sur exception tombera de lui-même !

Nan pas encore. Faut que je fasse un dossier complet dessus mais j'ai pas fini le dossier tests unitaires.

...