9.1 Création de dépendances cycliques de module
Supposons que tu as deux fichiers, a.py
et b.py
, chacun important l'autre comme suit :
Dans a.py
:
import b
def f():
return b.x
print(f())
Dans b.py
:
import a
x = 1
def g():
print(a.f())
Commençons par essayer d'importer a.py
:
import a
# 1
Ça a super bien marché. Ça te surprend peut-être. Après tout, les modules s'importent cycliquement, et ça devrait probablement poser problème, non ?
La réponse est que le simple fait d'avoir un import cyclique de modules n'est pas un problème en soi en Python. Si un module a déjà été importé, Python est assez malin pour ne pas essayer de l'importer à nouveau. Cependant, selon le moment où chaque module essaie d'accéder aux fonctions ou aux variables définies dans l'autre, tu peux vraiment rencontrer des problèmes.
Donc, revenant à notre exemple, lorsque nous avons importé a.py
, il n'a eu aucun problème à importer b.py
, car b.py
n'exige rien de a.py
pendant son importation. La seule référence dans b.py
à a
est l'appel à a.f()
. Mais cet appel est dans g()
, et rien dans a.py
ou b.py
n'appelle g()
. Ainsi tout fonctionne parfaitement.
Mais que se passe-t-il si nous essayons d'importer b.py
(sans avoir importé préalablement a.py
, c'est-à-dire) :
import b
Traceback (most recent call last):
File "<stdin>", line 1, in
File "b.py", line 1, in
import a
File "a.py", line 6, in
print(f())
File "a.py", line 4, in f
return b.x
AttributeError: 'module' object has no attribute 'x'
Le problème ici est que dans le processus d'importation de b.py
, il essaie d'importer a.py
, qui à son tour appelle f()
, qui essaie d'accéder à b.x
. Mais b.x
n'est pas encore défini. D'où l'exception AttributeError
.
Au moins, une des solutions à ce problème est assez triviale. Il suffit de modifier b.py
pour importer a.py
dans g()
:
x = 1
def g():
import a # Cela ne sera évalué que lorsque g() sera appelé
print(a.f())
Maintenant, quand nous importons, tout va bien :
import b
b.g()
# 1 Imprimé une première fois puisque le module 'a' appelle 'print(f())' à la fin
# 1 Imprimé une deuxième fois, ceci est notre appel à 'g()'
9.2 Conflit de noms avec les modules de la bibliothèque standard Python
L'un des charmes de Python est la multitude de modules livrés "prêts à l'emploi". Mais en conséquence, si tu ne fais pas attention, tu pourrais te retrouver avec un conflit de nom entre ton module et un module de la bibliothèque standard Python (par exemple, dans ton code tu pourrais avoir un module nommé email.py
, qui entrerait en conflit avec le module de la bibliothèque standard portant le même nom).
Cela peut causer de sérieux problèmes. Par exemple, si un module tente d'importer la version du module provenant de la bibliothèque standard Python, et que tu as un module portant le même nom dans ton projet, il importera par erreur ton module au lieu de celui de la bibliothèque standard.
Il est donc conseillé de faire attention à ne pas utiliser les mêmes noms que ceux des modules de la bibliothèque standard Python. Il est beaucoup plus simple de changer le nom du module dans ton projet que de demander à la communauté de Python de changer le nom du module dans la bibliothèque standard et d'attendre son approbation.
9.3 Visibilité des exceptions
Considérons le fichier suivant main.py
:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int("1"))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()
Ça semble correct, le code devrait fonctionner, voyons ce qu'il imprime :
$ python main.py 1
Traceback (most recent call last):
File "C:\Projects\Python\TinderBolt\main.py", line 19, in <module>
bad()
File "C:\Projects\Python\TinderBolt\main.py", line 17, in bad
print(e)
^
UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
Qu'est-ce qui vient de se passer ici ? Le problème est qu'en Python l'objet dans le bloc d'exception n'est pas accessible en dehors de celui-ci. (La raison en est que sinon, les objets dans ce bloc seraient conservés en mémoire jusqu'à ce que le ramasse-miettes soit déclenché et supprime les références qui y sont faites).
Une manière d'éviter ce problème est de conserver une référence à l'objet du bloc d'exception en dehors de ce bloc, afin qu'il reste accessible. Voici une version de l'exemple précédent qui utilise cette technique, rendant ainsi le code fonctionnel :
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int("1"))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()
9.4 Mauvaise utilisation de la méthode __del__
Lorsque l'interpréteur supprime un objet, il vérifie s'il a une fonction __del__
, et s'il en a une, il l'appelle avant de supprimer l'objet. C'est très pratique lorsque tu veux que ton objet nettoie certaines ressources externes ou un cache.
Supposons que tu as un fichier comme ceci mod.py
:
import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)
Et tu essaies de faire ça depuis un autre fichier another_mod.py
:
import mod
mybar = mod.Bar()
Et tu obtiens une horrible AttributeError
.
Pourquoi ? Parce que, comme indiqué ici, lorsque l'interpréteur termine son exécution, toutes les variables globales du module ont la valeur None
. En conséquence, dans l'exemple ci-dessus, au moment de l'appel de __del__
, le nom foo
a déjà été défini sur None
.
La solution à ce "casse-tête" consiste à utiliser la méthode spéciale atexit.register()
. Ainsi, lorsque ton programme se termine (c'est-à-dire lors d'une sortie normale), tes handle's
sont supprimés avant que l'interpréteur ne termine.
En tenant compte de cela, une correction du code mod.py
pourrait ressembler à ceci :
import foo
import atexit
def cleanup(handle):
foo.cleanup(handle)
class Bar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)
Cette implémentation fournit un moyen simple et fiable d'appeler tout nettoyage nécessaire après la fin normale du programme. Évidemment, la décision de ce qu'il faut faire avec l'objet lié au nom self.myhandle
revient à foo.cleanup
, mais je pense que tu as compris l'idée.
GO TO FULL VERSION