Les imports en Python
jeudi 16 mai 2013 à 11:13Je suis fan de carmina burrana depuis l’age de 12 ans, alors pourquoi pas O Fortuna comme musique d’ambiance :
Les imports, c’était fastoche. Vous étiez dans votre petit programme, et pour importer un module de la lib standard, vous faisiez:
import module
Par exemple :
import os
Et pour importer une classe ou une fonction de cette lib, vous faisiez :
from module import fonction from module import Classe
Par exemple :
from hashlib import md5 from xml.etree import Element
Parfois, c’était un peu plus compliqué, mais ça allait encore. Des fois il fallait importer un sous-module :
from package.sous_package import module
Par exemple :
from xml.sax import saxutils
Mais ça allait encore.
Et puis un jour vous avez du écrire votre propre module. Vous n’aviez pas vraiment réfléchi à la question. C’était juste une petite lib pour regrouper des fonctions. Ou juste une app Django. Un truc tout simple. Mais les imports ont soudainement cessé de devenir clairs. Ça ne marchait pas. Rien ne marchait. Vous aviez des sys.path.append
partout juste au cas où et c’était encore pire.
Vous avez donc décidé de vous remettre à PHP, au moins le include
utilise les chemins de fichiers, et ça, c’est facile.
Sous le capot
Quand vous utilisez import
, sous le capot Python utilise le fonction __import__
. Malgré ses __
dans le nom, c’est une fonction ordinaire, et vous pouvez d’ailleurs l’utiliser vous-même :
>>> os = __import__('os') >>> os.path.join('s', 'ton', 'mon', 'g') u's/ton/mon/g'
En fait, importer un module, c’est créer un objet module qui est assigné à une variable tout à fait normale :
>>> type(os) <type 'module'> >>> os = "on peut ecraser un module" >>> os.path Traceback (most recent call last): File "<ipython-input-12-e34748f24345>", line 1, in <module> os.path AttributeError: 'unicode' object has no attribute 'path' >>> import sys >>> type(sys) <type 'module'> >>> sys = "je t'ecrase la tronche" >>> type(sys) <type 'unicode'>
Le mécanisme de module Python n’est donc pas un truc à part, c’est un objet comme le reste, qui contient des attributs. Les attributs sont les variables et les fonctions du module.
Pour charger un module, la fonction __import__
passe par les étapes suivantes :
- Chercher si le module
os
existe. - Chercher si le module a déjà été importé. Si oui, s’arrêter ici et renvoyer le module existant.
- Si non, chercher si il a été déjà compilé en .pyc.
- Si ce n’est pas le cas, compiler le fichier .py en .pyc.
- Charger le bytecode du fichier pyc.
- Créer un objet module vide.
- Éxecuter le bytecode dans le contexte de l’objet module et remplir ce dernier avec le résultat.
- Ajouter l’objet module dans
sys.modules
, un dictionnaire qui contient tous les modules déjà chargés. - Retourner le module pour pouvoir l’assigner à une variable, par défaut la variable porte son nom.
La fonction __import__
est donc très complexe, et d’ailleurs si vous voulez l’utiliser pour des trucs plus compliqués qu’un simple import de module, vous allez galérer car sa signature est vraiment zarb.
Mais pour vous, seule l’étape 1 est importante à comprendre. C’est l’étape à laquelle tout se joue.
Comment Python définit quel module importer ?
C’est la partie vraiment difficile, en effet si un import ne marche pas, c’est très souvent parce que Python ne trouve pas le module que vous voulez. Et la raison pour laquelle il ne le trouve pas, c’est que vous ne comprenez pas comment il cherche.
Python utilise ce qu’on appelle le PYTHON PATH pour chercher les modules importables. C’est une variable système qui contient une liste de dossiers. Par exemple, sur ma machine, elle contient ceci :
['', '/usr/bin', '/usr/local/lib/python2.7/dist-packages/grin-1.2.1-py2.7.egg', '/usr/lib/python2.7', '/usr/lib/python2.7/plat-linux2', '/usr/lib/python2.7/lib-tk', '/usr/lib/python2.7/lib-old', '/usr/lib/python2.7/lib-dynload', '/home/sam/.local/lib/python2.7/site-packages', '/usr/local/lib/python2.7/dist-packages', '/usr/local/lib/python2.7/dist-packages/setuptools-0.6c11-py2.7.egg-info', '/usr/lib/python2.7/dist-packages', '/usr/lib/python2.7/dist-packages/PIL', '/usr/lib/python2.7/dist-packages/gst-0.10', '/usr/lib/python2.7/dist-packages/gtk-2.0', '/usr/lib/pymodules/python2.7', '/usr/lib/python2.7/dist-packages/ubuntu-sso-client', '/usr/lib/python2.7/dist-packages/ubuntuone-client', '/usr/lib/python2.7/dist-packages/ubuntuone-control-panel', '/usr/lib/python2.7/dist-packages/ubuntuone-couch', '/usr/lib/python2.7/dist-packages/ubuntuone-installer', '/usr/lib/python2.7/dist-packages/ubuntuone-storage-protocol', '/usr/lib/python2.7/dist-packages/wx-2.6-gtk2-unicode', '/usr/lib/python2.7/dist-packages/IPython/extensions']
Donc, quand vous faites import os
, Python va faire une boucle for
là dessus et chercher dans chaque dossier si un package (un dossier avec un fichier __init__.py
) ou un module (un fichier avec l’extension .py) nommé os
existe.
Dès qu’il en trouve un, il s’arrête de chercher et l’importe. Si il n’en trouve pas, il va lever une ImportError
.
Ce qui signifie que si votre module n’est PAS dans le PYTHON PATH, vous ne pouvez PAS l’importer. C’est impossible.
La grande majorité des problèmes d’import vient du fait que le module que vous essayez d’importer n’est pas dans le PYTHON PATH.
Maintenant, la grande question, c’est :
Qu’est-ce qui est dans le PYTHON PATH ?
Par défault, les dossiers sites-packages et dist-packages dans le dossier d’installation Python sont dans le PYTHON PATH. Quelques autres sont ajoutés selon les systèmes, mais vous pouvez toujours compter sur sites-packages et dist-packages pour être dans le PYTHON PATH. Quand vous installez une lib, par exemple avec pip, c’est là dedans que la lib va s’installer, pour être sûre de pouvoir être importée.
Quand vous êtes dans un virtualenv, les dossiers sites-packages et dist-packages de l’environnement virtuel sont ajoutés au PYTHON PATH.
Mais tout ça ne change pas grand chose pour vous. En effet, vous n’allez pas mettre VOTRE code dans les dossiers sites-packages et dist-packages.
C’est pour cela que Python possède un mécanisme supplémentaire : le dossier qui contient le module sur lequel vous lancez la commande python
est automatiquement ajouté au PYTHON PATH.
Le PYTHON PATH, en pratique
Supposons que je sois dans le dossier /home/sam/Bureau et que j’aie dedans ce package. Voici à quoi ressemble mon arbo (téléchargez l’arbo vierge pour vos tests):
/home/sam/Bureau # <-- je suis ici . `-- test_imports |-- __init__.py |-- package_tout_en_haut | |-- __init__.py | |-- autre_sous_package | | |-- __init__.py | | `-- autre_module_en_bas.py | |-- sous_module.py | `-- sous_package | |-- __init__.py | |-- autre_module_en_bas.py | |-- autre_sous_package | | |-- __init__.py | | `-- autre_module_en_bas.py | `-- module_tout_en_bas.py `-- top_module.py
Si je lance un shell Python depuis ce dossier ou un script Python contenu dans ce dossier, je peux faire import test_imports
, car /home/sam/Bureau est automatiquement ajouté au PYTHON PATH.
Je peux donc faire :
>>> import test_imports >>> from test_imports import package_tout_en_haut >>> from test_imports import top_module test_imports.top_module >>> from test_imports.package_tout_en_haut import sous_module test_imports.package_tout_en_haut.sous_module
Mais si je me mets ici dans ./package_tout_en_haut/sous_package :
/home/sam/Bureau . `-- test_imports |-- __init__.py |-- package_tout_en_haut | |-- __init__.py | |-- autre_sous_package | | |-- __init__.py | | `-- autre_module_en_bas.py | |-- sous_module.py | `-- sous_package # <-- je suis ici | |-- __init__.py | |-- autre_module_en_bas.py | |-- autre_sous_package | | |-- __init__.py | | `-- autre_module_en_bas.py | |-- module_tout_en_bas.py `-- top_module.py
Je ne peux PAS importer test_imports
, ni dans un shell, ni depuis un module de ce dossier :
>>> import test_imports Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: No module named test_imports
En effet, comme je lance la commande Python depuis
./package_tout_en_haut/sous_package
alors
./package_tout_en_haut/sous_package
EST ajouté au PYTHON PATH, mais
/home/sam/Bureau/
n’est PAS ajouté au PYTHON PATH.
Je ne peux donc PAS faire
from test_imports import top_module
depuis un fichier comme
.test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py
et exécuter directement
python autre_module_en_bas.py
ni même
python ./test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py
Je peux faire
from test_imports import top_module
depuis
autre_module_en_bas.py
uniquement si je lance un script Python tout en haut de mon arbo qui importe
autre_module_en_bas.py.
Mais alors, comment on fait ?
Il faut s’assurer que le dossier qui contient test_imports
, notre module racine, soit TOUJOURS dans le PYTHON PATH.
Il y a plusieurs possibilités pour cela.
La première, c’est que notre lib va être utilisée une fois installée avec pip. Dans ce cas, on s’en branle, test_imports
sera dans sites-packages automatiquement, et on pourra faire from test_imports import top_module
de partout joyeusement.
Mais souvent, ce n’est pas le cas, votre code n’est pas fait pour être installé.
La seconde technique consiste à s’assurer que l’on appelle TOUJOURS la commande Python depuis le dossier qui est tout au dessus. C’est ce que fait django avec sa commande ./manage.py
par exemple.
Vous avez votre projet :
./manage.py projet
Et tout passe par python manage.py
, qui est au dessus de projet, donc le dossier est bien ajouté au PYTHON PATH, et tout va bien.
Dans votre cas ça veut dire vous assurer qu’on lance toujours votre programme depuis un script d’entrée qui est tout en haut de votre arborescence.
Ca veut dire que vous devez avoir un point d’entrée UNIQUE pour votre package.
Mais parfois ça ne convient pas. Dans le cas des tests unitaires par exemple, il vous faut un point d’entrée spécialement pour les tests.
Pour ce genre de scénario, il faut donc avoir le dossier qui les contient à côté de votre package. Ainsi, si j’avais des tests unitaires, je devrais faire un dossier tests à côté du dossier test_imports. Par exemple, transformer mon arbo en un truc comme ça :
src |_ test_imports |_ tests
Afin que je lance les tests en faisant python tests
depuis src. Et dans mes fichiers de tests, je pourrai faire des from test_imports import truc
.
La manière dont vous organisez votre projet est donc très importante en Python, et si vous avez des problèmes d’import, la première chose à faire est de changer sa structure. Il n’y a pas de magie.
La dernière possibilité, quand tout a échoué, c’est de rajouter à la main le dossier dans le PYTHON PATH. sys.path
est une simple liste, on peut donc faire un append()
dessus.
Par exemple, si je veux absolument (mais je ne devrais pas :-)) pouvoir faire :
python .test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py
et importer test_imports
dans autre_module_en_bas.py, je peux faire un truc du genre :
import os dossier = os.path.dirname(os.path.abspath(__file__)) while not dossier.endswith('test_imports'): dossier = os.path.dirname(dossier) dossier = os.path.dirname(dossier) if dossier not in sys.path: sys.path.append(dossier)
Ce code va remonter dans l’arbo jusqu’à tomber sur le chemin du dossier test_imports et ajouter son dossier parent au PYTHON PATH.
Ce n’est pas le truc le plus propre du monde, mais ça peut dépanner.
Imports absolus et relatifs
Si vous êtes dans ./package_tout_en_haut/sous_package :
/home/sam/Bureau . `-- test_imports |-- __init__.py |-- package_tout_en_haut | |-- __init__.py | |-- autre_sous_package | | |-- __init__.py | | `-- autre_module_en_bas.py | |-- sous_module.py | `-- sous_package # <-- je suis ici | |-- __init__.py | |-- autre_module_en_bas.py | |-- autre_sous_package | | |-- __init__.py | | `-- autre_module_en_bas.py | |-- module_tout_en_bas.py | `-- test_imports # <-- autre package nommé test_imports | `-- sous_module.py `-- top_module.py
Vous voyez que vous avez deux packages nommés test_imports.
Si vous écrivez import test_imports
dans autre_module_en_bas.py, que va-t-il se passer ?
C'est le module tout en bas qui va être importé.
Ce n'est pas forcément ce que vous voulez. Python 3 corrige cela en permettant des imports relatifs, et Python 2.7 peut en bénéficier en important tout en haut du module :
from __future__ import absolute_import
En faisant cela, vous obtenez le comportement de Python 3 dans Python 2.7, et vous pourrez alors choisir entre faire :
import test_imports # importe le module tout en haut import .test_imports # import le module dans le même dossier from .test_imports import sous_module from test_imports import top_module
Je vous recommande de toujours utiliser from __future__ import absolute_import. Ca ne coûte rien, et c'est plus cohérent. Par contre, vous ne pourrez pas tester from __future__ import absolute_import dans le shell, donc cet exemple ne marche pas dans ipython, mais il fonctionne parfaitement dans vos modules.
On peut aussi faire des imports relatifs du package contenant avec :
from .. import truc from ..package import machin
N'oubliez pas que ceci ne marche que :
- Si
from __future__ import absolute_import
est activé. - Le package tout en haut (celui qui contient tous les autres) est dans le PYTHON PATH
Sinon, ça ne sert A RIEN. Ce n'est pas comme un ../ dans un bash. Ça ne remonte pas d'un dossier. C'est juste une notation pour dire j'utilise celui la plutôt que l'autre, quand il y a ambiguité.
Pièges des imports
Package sans init
Si vous avez :
. `-- test_imports |-- __init__.py |-- package_sans_init | `-- nada.py
nada.py n'est pas importable, car package_sans_init ne contient pas de fichier __init__.py, même si test_imports est dans le PYTHON PATH. Ce comportement est corrigé en Python 3, et tout sous-dossier d'un package importable est automatiquement importable, qu'il contienne un __init__.py ou non.
Imports circulaires
J'en ai déjà parlé ici.
Vous avez :
. `-- test_imports |-- __init__.py |-- package_tout_en_haut | |-- __init__.py | `-- sous_package | |-- __init__.py | |-- autre_module_en_bas.py | `-- module_tout_en_bas.py
Et vous importez autre_module_en_bas
dans module_tout_en_bas
et inversement. Non seulement ça ne marchera pas, mais en plus l'erreur est déroutante :
ImportError: No module named module_tout_en_bas
Oui vous avez bien lu, il va vous dire que le module n'existe pas !
Il n'y a pas non plus de solution propre à ce problème : soit vous fusionnez vos deux fichiers, soit vous faites un 3eme module qui utilise ces deux modules (et ces deux modules n'importent pas ce 3eme module).
Sinon il y a la solution crade : mettre un des imports dans un appel de fonction ou de méthode comme ça:
def truc(): import module_tout_en_bas module_tout_en_bas.bidule()
Parfois, ça dépanne :-) On ne tue pas des chatons non plus, donc si ça ne devient pas une habitude, ça peut passer.