Est-ce que «finalement» s'exécute toujours en Python?

126

Pour tout bloc try-finally possible en Python, est-il garanti que le finallybloc sera toujours exécuté?

Par exemple, disons que je reviens dans un exceptbloc:

try:
    1/0
except ZeroDivisionError:
    return
finally:
    print("Does this code run?")

Ou peut-être que je relance un Exception:

try:
    1/0
except ZeroDivisionError:
    raise
finally:
    print("What about this code?")

Les tests montrent que finallycela est exécuté pour les exemples ci-dessus, mais j'imagine qu'il existe d'autres scénarios auxquels je n'ai pas pensé.

Existe-t-il des scénarios dans lesquels un finallybloc peut échouer à s'exécuter en Python?

Stevoisiak
la source
18
Le seul cas que je peux imaginer finallyéchouer à exécuter ou "vaincre son objectif" est pendant une boucle infinie, sys.exitou une interruption forcée. La documentation indique que cela finallyest toujours exécuté, alors j'irais avec ça.
Xay
1
Un peu de réflexion latérale et certainement pas ce que vous avez demandé, mais je suis presque sûr que si vous ouvrez le Gestionnaire des tâches et tuez le processus, finallyil ne fonctionnera pas. Ou la même chose si l'ordinateur plante avant: D
Alejandro
145
finallyne s'exécutera pas si le cordon d'alimentation est arraché du mur.
user253751
3
Vous pourriez être intéressé par cette réponse à la même question sur C #: stackoverflow.com/a/10260233/88656
Eric Lippert
1
Bloquez-le sur un sémaphore vide. Ne le signalez jamais. Terminé.
Martin James

Réponses:

206

«Garanti» est un mot beaucoup plus fort que toute implémentation de finallymérite. Ce qui est garanti que si l' exécution des flux de l'ensemble try- finallyconstruction, il passera par le finallyfaire. Ce qui n'est pas garanti, c'est que l'exécution découlera du try- finally.

  • Un finallydans un générateur ou une coroutine asynchrone peut ne jamais s'exécuter , si l'objet ne s'exécute jamais jusqu'à la conclusion. Il y a de nombreuses façons qui pourraient arriver; en voici une:

    def gen(text):
        try:
            for line in text:
                try:
                    yield int(line)
                except:
                    # Ignore blank lines - but catch too much!
                    pass
        finally:
            print('Doing important cleanup')
    
    text = ['1', '', '2', '', '3']
    
    if any(n > 1 for n in gen(text)):
        print('Found a number')
    
    print('Oops, no cleanup.')
    

    Notez que cet exemple est un peu délicat: lorsque le générateur est ramassé, Python tente d'exécuter le finallybloc en lançant une GeneratorExitexception, mais ici nous interceptons cette exception puis à yieldnouveau, à quel point Python imprime un avertissement ("générateur ignoré GeneratorExit ") et abandonne. Voir PEP 342 (Coroutines via des générateurs améliorés) pour plus de détails.

    D'autres façons dont un générateur ou une coroutine peuvent ne pas s'exécuter jusqu'à la conclusion incluent si l'objet n'est tout simplement jamais GC (oui, c'est possible, même en CPython), ou si un async with awaits in __aexit__, ou si l'objet awaits ou yields dans un finallybloc. Cette liste ne prétend pas être exhaustive.

  • Un finallydans un thread démon peut ne jamais s'exécuter si tous les threads non démon se terminent en premier.

  • os._exitarrêtera le processus immédiatement sans exécuter de finallyblocs.

  • os.forkpeut provoquer l' exécution desfinally blocs deux fois . En plus des problèmes normaux auxquels vous vous attendez lorsque les choses se passent deux fois, cela pourrait provoquer des conflits d'accès simultanés (plantages, blocages, ...) si l'accès aux ressources partagées n'est pas correctement synchronisé .

    Depuis multiprocessingutilise fork-without-exec pour créer des processus de travail lors de l'utilisation de la méthode de démarrage de fork (par défaut sous Unix), puis appelle os._exitle worker une fois que le travail du travailleur est terminé, finallyet l' multiprocessinginteraction peut être problématique ( exemple ).

  • Un défaut de segmentation de niveau C empêchera les finallyblocs de s'exécuter.
  • kill -SIGKILLempêchera les finallyblocs de fonctionner. SIGTERMet SIGHUPempêchera également les finallyblocs de s'exécuter sauf si vous installez un gestionnaire pour contrôler vous-même l'arrêt; par défaut, Python ne gère pas SIGTERMou SIGHUP.
  • Une exception dans finallypeut empêcher le nettoyage de se terminer. Un cas particulièrement remarquable est si l'utilisateur frappe control-C juste au moment où nous commençons à exécuter le finallybloc. Python lèvera a KeyboardInterruptet sautera chaque ligne du finallycontenu du bloc. ( KeyboardInterrupt-safe code est très difficile à écrire).
  • Si l'ordinateur perd de la puissance, ou s'il hiberne et ne se réveille pas, les finallyblocs ne fonctionneront pas.

Le finallybloc n'est pas un système de transaction; il ne fournit aucune garantie d'atomicité ou quoi que ce soit de ce genre. Certains de ces exemples peuvent sembler évidents, mais il est facile d'oublier que de telles choses peuvent arriver et finallyde trop se fier .

user2357112 prend en charge Monica
la source
14
Je crois que seul le premier point de votre liste est vraiment pertinent, et il y a un moyen simple de l'éviter: 1) ne jamais utiliser un nu except, et ne jamais attraper à l' GeneratorExitintérieur d'un générateur. Les points sur les threads / tuer le processus / segfaulting / power off sont attendus, python ne peut pas faire de magie. Aussi: les exceptions dans finallysont évidemment un problème mais cela ne change pas le fait que le flux de contrôle a été déplacé vers le finallybloc. En ce qui concerne Ctrl+C, vous pouvez ajouter un gestionnaire de signal qui l'ignore, ou simplement "planifier" un arrêt propre une fois l'opération en cours terminée.
Giacomo Alzetta
8
La mention de kill -9 est techniquement correcte, mais un peu injuste. Aucun programme écrit dans aucune langue n'exécute de code après avoir reçu un kill -9. En fait, aucun programme ne reçoit jamais de kill -9 du tout, donc même s'il le voulait, il ne pouvait rien exécuter. C'est tout l'intérêt de tuer -9.
Tom
10
@Tom: Le point sur kill -9n'a pas spécifié de langue. Et franchement, il faut le répéter, car il se trouve dans un angle mort. Trop de gens oublient, ou ne se rendent pas compte, que leur programme pourrait être stoppé net sans même être autorisé à nettoyer.
cHao
5
@GiacomoAlzetta: Il y a des gens qui s'appuient sur des finallyblocs comme s'ils offraient des garanties transactionnelles. Cela peut sembler évident que non, mais ce n'est pas quelque chose que tout le monde réalise. En ce qui concerne le cas du générateur, il y a beaucoup de façons un générateur pourrait ne pas être GC'ed du tout, et beaucoup de façons un générateur ou coroutine peut accidentellement donner , après GeneratorExitmême si elle ne se coince pas GeneratorExit, par exemple , si un async withsuspend une coroutine en __exit__.
user2357112 prend en charge Monica
2
@ user2357112 ouais - j'essaie depuis des décennies de demander aux développeurs de nettoyer les fichiers temporaires, etc. au démarrage de l'application, pas de quitter. S'appuyer sur le soi-disant `` nettoyage et résiliation gracieuse '', c'est demander la déception et les larmes :)
Martin James
68

Oui. Enfin gagne toujours.

La seule façon de le vaincre est d'arrêter l'exécution avant d' finally:avoir une chance de s'exécuter (par exemple, planter l'interpréteur, éteindre votre ordinateur, suspendre un générateur pour toujours).

J'imagine qu'il y a d'autres scénarios auxquels je n'ai pas pensé.

En voici quelques autres auxquels vous n'avez peut-être pas pensé:

def foo():
    # finally always wins
    try:
        return 1
    finally:
        return 2

def bar():
    # even if he has to eat an unhandled exception, finally wins
    try:
        raise Exception('boom')
    finally:
        return 'no boom'

Selon la façon dont vous quittez l'interpréteur, vous pouvez parfois "annuler" enfin, mais pas comme ça:

>>> import sys
>>> try:
...     sys.exit()
... finally:
...     print('finally wins!')
... 
finally wins!
$

Utilisation du précaire os._exit(cela relève de "crash l'interpréteur" à mon avis):

>>> import os
>>> try:
...     os._exit(1)
... finally:
...     print('finally!')
... 
$

J'exécute actuellement ce code, pour tester s'il s'exécutera enfin après la mort par la chaleur de l'univers:

try:
    while True:
       sleep(1)
finally:
    print('done')

Cependant, j'attends toujours le résultat, alors revenez ici plus tard.

wim
la source
5
ou ayant une boucle i finie dans try catch
sapy
8
finallydans un générateur ou une coroutine peut facilement échouer à s'exécuter , sans se rapprocher d'une condition de "crash de l'interpréteur".
user2357112 prend en charge Monica
27
Après que la mort par la chaleur de l'univers cesse d'exister, il en sleep(1)résulterait certainement un comportement indéfini. :-D
David Foerster
Vous voudrez peut-être mentionner _os.exit directement après «le seul moyen de le vaincre est de planter le compilateur». À l'heure actuelle, c'est mélangé entre des exemples où finalement gagne.
Stevoisiak
2
@StevenVascellaro Je ne pense pas que ce soit nécessaire - os._exitc'est, à toutes fins pratiques, la même chose que d'induire un crash (sortie impure). La bonne façon de sortir est sys.exit.
wim
9

Selon la documentation Python :

Peu importe ce qui s'est passé précédemment, le bloc final est exécuté une fois le bloc de code terminé et toutes les exceptions levées gérées. Même s'il y a une erreur dans un gestionnaire d'exceptions ou le bloc else et qu'une nouvelle exception est déclenchée, le code dans le bloc final est toujours exécuté.

Il convient également de noter que s'il y a plusieurs instructions return, dont une dans le bloc finally, alors le retour de bloc finally est le seul qui s'exécutera.

Jayce
la source
8

Eh bien, oui et non.

Ce qui est garanti, c'est que Python essaiera toujours d'exécuter le bloc finally. Dans le cas où vous revenez du bloc ou déclenchez une exception non interceptée, le bloc finally est exécuté juste avant de renvoyer ou de lever l'exception.

(ce que vous auriez pu contrôler vous-même en exécutant simplement le code de votre question)

Le seul cas où je peux imaginer où le bloc finally ne sera pas exécuté est lorsque l'interpréteur Python lui-même se bloque, par exemple dans le code C ou à cause d'une panne de courant.

Serge Ballesta
la source
ha ha .. ou il y a une boucle infinie dans try catch
sapy
Je pense que "Eh bien, oui et non" est le plus correct. Finalement: gagne toujours où "toujours" signifie que l'interpréteur est capable de s'exécuter et que le code pour "finalement:" est toujours disponible, et "gagne" est défini comme l'interpréteur tentera d'exécuter le bloc finally: et il réussira. C'est le «oui» et c'est très conditionnel. "Non" est toutes les façons dont l'interpréteur pourrait s'arrêter avant "finalement:" - panne de courant, panne matérielle, kill -9 visant l'interpréteur, erreurs dans l'interpréteur ou dans le code dont il dépend, autres façons de bloquer l'interpréteur. Et des moyens de s'accrocher à l'intérieur du "enfin:".
Bill IV
1

J'ai trouvé celui-ci sans utiliser de fonction générateur:

import multiprocessing
import time

def fun(arg):
  try:
    print("tried " + str(arg))
    time.sleep(arg)
  finally:
    print("finally cleaned up " + str(arg))
  return foo

list = [1, 2, 3]
multiprocessing.Pool().map(fun, list)

Le sommeil peut être n'importe quel code qui pourrait s'exécuter pendant des périodes de temps incohérentes.

Ce qui semble se produire ici, c'est que le premier processus parallèle à terminer quitte le bloc try avec succès, mais tente ensuite de renvoyer à partir de la fonction une valeur (foo) qui n'a été définie nulle part, ce qui provoque une exception. Cette exception tue la carte sans permettre aux autres processus d'atteindre leur bloc final.

Aussi, si vous ajoutez la ligne bar = bazzjuste après l'appel sleep () dans le bloc try. Ensuite, le premier processus pour atteindre cette ligne lève une exception (parce que bazz n'est pas défini), ce qui provoque l'exécution de son propre bloc finally, mais tue ensuite la carte, faisant disparaître les autres blocs try sans atteindre leurs blocs finalement, et le premier processus à ne pas atteindre son instruction de retour non plus.

Ce que cela signifie pour le multitraitement Python, c'est que vous ne pouvez pas faire confiance au mécanisme de gestion des exceptions pour nettoyer les ressources dans tous les processus si même l'un des processus peut avoir une exception. Un traitement de signal supplémentaire ou une gestion des ressources en dehors de l'appel de mappage multitraitement serait nécessaire.

Blair Houghton
la source
-2

Addendum à la réponse acceptée, juste pour vous aider à voir comment cela fonctionne, avec quelques exemples:

  • Ce:

     try:
         1
     except:
         print 'except'
     finally:
         print 'finally'

    sortira

    enfin

  •    try:
           1/0
       except:
           print 'except'
       finally:
           print 'finally'

    sortira

    sauf
    finalement

Basj
la source
2
Cela ne répond pas du tout à la question. Il a juste des exemples de quand cela fonctionne .
zixuan