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?
"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 with
await
s dans __aexit__
, ou si l'objet await
s ou yield
s 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 ).
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
.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).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
.
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.
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.
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.
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.
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