Les imports en Python 24


Je 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 :

  1. Chercher si le module os existe.
  2. Chercher si le module a déjà été importé. Si oui, s’arrêter ici et renvoyer le module existant.
  3. Si non, chercher si il a été déjà compilé en .pyc.
  4. Si ce n’est pas le cas, compiler le fichier .py en .pyc.
  5. Charger le bytecode du fichier pyc.
  6. Créer un objet module vide.
  7. Éxecuter le bytecode dans le contexte de l’objet module et remplir ce dernier avec le résultat.
  8. Ajouter l’objet module dans sys.modules, un dictionnaire qui contient tous les modules déjà chargés.
  9. 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
from . 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.

24 thoughts on “Les imports en Python

  • Sam Post author

    J’ai mal géré mon temps et je dois me casser attraper un train. Pas le temps de relire donc. Si une bonne âme veut passer par là, se serait super cool !

  • entwanne

    Petite erreur, dans la liste des actions faites par `__import__`: le point 3. devrait commencer par «Si non»

  • kontre

    Ayé, relu. Ça se sent quand tu ne relis pas ! ^^

    L’article est bon, mais on s’y perd facilement dans les noms de fichiers, genre dans les derniers paragraphes de “Le PYTHON PATH, en pratique”.

    Ce que c’est relou les imports circulaires. Ça force à grouper des choses fondamentalement différentes mais qui travaillent ensemble. Heureusement que le duck typing permet parfois de l’éviter.

  • Sam Post author

    J’ai updaté l’article puor rendre les noms de fichiers plus lisibbles, fournir une arbo de test et mettre un peu de zik pour faire passer la pillule.

    Tu vois autre chose à faire kontre ?

  • martin

    Après avoir beaucoup chipoté en plaçant mes modules personnels dans le dossier site_package, ou en ajoutant le dossier contenant le module dans le PYTHONPATH avec un EXPORT dans mon fichier .bashrc ou bash_profile, je me suis finalement tourné vers les fichiers .pth à placer dans site-package (voir un article ancien de Bob Ipolito Using .pth files for Python development).

    Il y en a de plusieurs types dont le plus simple contient simplement le chemin du dossier à ajouter (messcrits.pth):

    /Users/Shared/Dropbox/messcripts

    et qui me donne accès à toutes les classes et les fonctions qui sont dans les fichiers présents (comme il est dans Dropbox, je peux m’en servir sur toutes les machines que j’utilise, Linux et Mac OS X, il suffit d’utiliser sur chacune un fichier .pth pointant vers le dossier).

    Il y a moyen d’ajouter plusieurs dossiers (un par ligne) comme dans (Django les supporte aussi, voir A Note on Python Paths ).
    /Library/test/django_src/trunk
    /Library/tes/myproject_src/trunk

    D’autres compliquent un peu les choses en écrivant:
    import sys; sys.path.insert(0,'/Library/Frameworks/GDAL.framework/Versions/1.9/Python/2.7/site-packages')
    pour être surs que leur module soit en première position dans la liste du PYTHONPATH.

    Et enfin, certains utilisent cette technique pour changer l’appel du module comme le fichier Ngl.pth qui ne contient que PyNGL, ce qui permet d’utiliser soit
    import Ngl
    soit
    import PyNGL

  • glickind

    On peut rappeler explicitement que faire un import exécute le code du fichier/module importé et faire un lien sur votre article:
    Pourquoi if __name__ == ‘__main__’ en Python ?

    Par ailleurs, comme tu as demandé une relecture:
    s/Mais si je me met ici/Mais si je me mets ici/
    s/quand tout à échoué/quand tout a échoué/

  • Etienne

    C’est fou le nombre de fois où je me suis dit: “Y’a un truc à ce sujet sur S&M”, alors je google: “décorateurs site:sametmax.com”, par exemple.

    Pour dire, même si je google ‘”à poil les putes” site:sametmax.com’ je tombe sur des trucs vachement intéressants:

    4 résultats:
    – Quel hébergement Web pour les projets Python ?
    – Monitorez vos serveurs avec munin et notifications par email
    – Concurrence sans threads en python
    – FizzBuzz en Python

    Comme quoi on peut voir sa bite en gros plan et avoir aussi un cerveau…

  • Yohann

    Je viens relire cet article depuis celui sur le nouveau snipet python 3 et je tombe sur ça:

    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

    c’est pas plustôt les import absolus justement que permet python3 ?

    Encore merci pour vos articles, continuez les mecs!

  • Sam Post author

    C’est la même chose: tu as la synaxes des imports relatifs uniquement si tu as les imports absolus. Sinon les imports sont des imports absotifs :-) Ni absolus, ni relatifs, mais à l’arrache ^^

  • Mat

    Salut,

    Merci pour le tutoriel :)

    Par contre à un moment, vous dîtes :

    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.

    Cependant, j’ai créé la même arborescence (pour faire des tests unitaires), avec:

    test_import
    ………..|_ __init__py
    ………..|_ src
    ……………..|_ __init__
    ……………..|_ code.py
    ………..|_ tests
    ……………..|_ __init__py
    ……………..|_ test_code.py

    Tous les __init__.py sont vides.
    Dans test_code.py, j’ai mis “from src import code”

    Puis, en me plaçant dans le répertoire test_import dans une console, je fais:
    > python tests/test_code.py

    Et j’obtiens un joli ImportError, alors que c’est censé marcher d’après le paragraphe cité ci-dessus…
    En fait, j’ai testé en imprimant le sys.path, et il ajoute “test_import/tests” et non “test_import” comme indiqué…

    Qu’est ce que j’ai mal fait ?

    Mon but final est de pouvoir utiliser nosetests depuis n’importe où dans l’arborescence, sans avoir à faire des “sys.path.append()” de partout.

    Merci pour votre aide :) !

  • Sam Post author

    Il faut toujours lancer la commande nose depuis le repertoire racine du projet, pas à l’interieur. Si tu veux lancer un test en particulier, tape le chemin vers ce fichier, relativement à la racine.

  • Marco

    C’est coool s’t’article, j’étais venu un peu pour du q et puis au final je vais me mettre un peu au langage Python…

  • Sam Post author

    sametmax: on vient pour les godes, on reste pour le code !

  • Anto

    Hey ! Merci pour l’excellent article.

    Juste une curiosité au niveau de la variable PYTHONPATH: est-ce qu’il s’agit d’une variable d’environnement comme les autres ? Sous Ubuntu:

    $ workon myenv
    $ ipython
     
    In [1]: import sys; sys.path
    Out[1]: 
    ['', 
     '/usr/lib/python2.7',
     '/usr/lib/python2.7/plat-x86_64-linux-gnu',
     '/usr/lib/python2.7/lib-tk',
     # etc.
    ]
     
    # Mais pourtant
    $ printenv | grep -i python  # ne retourne rien

    J’aurais cru que virtualenvwrapper peuplerait la variable à la volée, mais ça n’a pas l’air d’être le cas.

  • Inzaguiz

    est-ce qu’on peut importer une fonction dans un autre fichier.py que le fichier “main” dans même dossier ? Comment ?

  • Nightingale

    Hola hola,

    merci pour l’article! :)

    Petite question à laquelle je ne parviens pas à trouver une réponse, même avec l’aide de mon meilleur ami google =/

    Est-il possible de connaitre le nom du script faisant l’import d’un module?

    En somme j’aimerais qu’un module puisse avoir un comportement différent si c’est super_script1.py qui veut l’importer ou si c’est super_script2.py o_O

  • sakkal

    Always i use

    sys.path.append(os.path.join(os.path.dirname(file),configFile))

    And i declare all paramete global

Leave a comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.