web-dev-qa-db-fra.com

"Enfin" est-il toujours exécuté en Python?

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

Par exemple, disons que je retourne dans un bloc except:

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

Ou peut-être que je relance une Exception:

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

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

Y a-t-il des scénarios dans lesquels un bloc finally peut ne pas s'exécuter en Python?

100

"Garanti" est un mot beaucoup plus puissant que ne le mérite toute implémentation de finally. Ce qui est garanti, c'est que si l'exécution découle de l'ensemble de la construction try-finally, elle passera par la finally pour le faire. Ce qui n'est pas garanti, c'est que l'exécution découlera de try-finally.

  • Un finally dans 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 beaucoup de façons qui pourraient arriver; en voici un:

    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 mal ordonné, Python tente d’exécuter le bloc finally en lançant une exception GeneratorExit, mais dans ce cas, nous interceptons cette exception puis yield, auquel cas Python affiche un avertissement (" générateur ignoré GeneratorExit ") et abandonne. Voir PEP 342 (Coroutines via Enhanced Generators) pour plus de détails.

    Un générateur ou une coroutine peut ne pas être exécuté jusqu'à la fin, mais si l'objet n'est simplement jamais GC (oui, c'est possible, même dans CPython), ou si un async withawaits dans __aexit__, ou si l'objet awaits ou yields dans un finally bloc. Cette liste n’est pas exhaustive.

  • Un finally dans un thread de démon pourrait ne jamais s'exécuter si tous les threads autres que les démons sont fermés en premier.

  • os._exit arrêtera le processus immédiatement sans exécuter les blocs finally.

  • os.fork peut provoquer des blocs finally à exécuter deux fois . En plus des problèmes normaux que vous attendez de ce qui se passe deux fois, cela pourrait entraîner des conflits d'accès simultanés (plantages, blocages, ...) si l'accès aux ressources partagées n'est pas correctement synchronisé .

    Puisque multiprocessing utilise fork-without-exec pour créer des processus de travail en utilisant la méthode de démarrage fork (valeur par défaut sous Unix), puis appelle os._exit dans le travailleur une fois que le travail du travailleur est terminé, finally et multiprocessing l'interaction peut être problématique ( exemple ).

  • Une erreur de segmentation au niveau C empêchera les blocs finally de s'exécuter.
  • kill -SIGKILL empêchera les blocs finally de s'exécuter. SIGTERM et SIGHUP empêchent également les blocs finally 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 SIGTERM ni SIGHUP.
  • Une exception dans finally peut empêcher le nettoyage de se terminer. Un cas particulièrement remarquable est celui où l'utilisateur appuie sur control-C just alors que nous commençons à exécuter le bloc finally. Python va générer un KeyboardInterrupt et ignorer chaque ligne du contenu du bloc finally. (KeyboardInterrupt- le code sécurisé est très difficile à écrire).
  • Si l'ordinateur tombe en panne, ou s'il passe en veille et ne se réveille pas, les blocs finally ne fonctionneront pas.

Le bloc finally n'est pas un système de transaction; il ne fournit aucune garantie d'atomicité ni rien de ce genre. Certains de ces exemples peuvent sembler évidents, mais il est facile d’oublier que de telles choses peuvent se produire et s’appuyer trop sur finally.

165
user2357112

Oui. Enfin toujours gagne. 

La seule façon de le vaincre est d’arrêter l’exécution avant que finally: ne puisse s’exécuter (par exemple, bloquer l’interprète, é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é.

Voici un couple de plus que vous n'auriez 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ète, vous pouvez parfois "annuler" enfin, mais pas comme ceci:

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

Utiliser le précaire os._exit (à mon avis, cela tombe sous "crash l'interprète"):

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

J'exécute actuellement ce code pour tester si finalement sera toujours exécuté après la mort de l'univers par la chaleur:

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

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

63
wim

Selon la documentation Python :

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

Il convient également de noter que s'il existe plusieurs instructions de retour, dont une dans le bloc finally, le retour du dernier bloc est alors le seul à s'exécuter.

7
jayce

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 que vous levez une exception non capturé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 dans votre question)

Le seul cas où je peux imaginer où le bloc final ne sera pas exécuté est celui où l'interpréteur Python se bloque, par exemple dans le code C ou en raison d'une panne de courant.

6
Serge Ballesta

J'ai trouvé celui-ci sans utiliser une fonction de 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 pouvant être exécuté pendant une durée incohérente.

Ce qui semble se produire ici est que le premier processus parallèle terminé termine le bloc try avec succès, mais tente ensuite de renvoyer à 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 leurs derniers blocs.

De même, si vous ajoutez la ligne bar = bazz juste après l'appel sleep () dans le bloc try. Ensuite, le premier processus qui atteint cette ligne lève une exception (car bazz n’est pas défini), ce qui provoque l’exécution de son propre bloc final, puis tue la carte et fait disparaître les autres blocs d’essai sans atteindre leurs blocs définitifs. le premier processus à ne pas atteindre sa déclaration de retour, non plus.

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

0
Blair Houghton

Pour vraiment comprendre son fonctionnement, exécutez ces deux exemples:

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

    va sortir 

    enfin

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

    va sortir 

    sauf
    enfin

0
Basj