Cet article fait partie d’une série d’articles sur la syntaxe de base Python.
Le but de cet article est d’expliquer les imports de modules Python (les modules d’extension ne seront pas abordés).
Dans un premier temps, on va décrire brièvement le fonctionnement du système d’import de modules. Ensuite, on va compléter cette description avec un exemple. Enfin, on va rappeler la syntaxe pour importer un module et obtenir des informations sur un module importé.
Fonctionnement du système d’import
Mécanisme d’import des modules
Attributs des modules
Chemin des modules dans les packages
Encapsulation
Import d’un package
Python path
Import de modules
Avoir des informations sur un module importé
Les seuls objets Python sont les modules quelque soit leur implémentation:
- les modules purs implémentés en Python sous la forme de fichier
.py
, - les modules d’extensions en C,
- etc…
Les packages sont des objets Python pouvant s’apparenter à des répertoires. Ces packages contiennent des sub-packages ou des modules. Un package est un module avec un attribut __path__
.
Il existe une différence entre l’identification des modules et packages Python et le chemin des fichiers ou répertoires correspondant dans le système de fichiers. Ainsi il faut considérer les modules et les packages comme étant un type d’objet plutôt que comme des fichiers ou des répertoires.
Par suite, les règles suivantes s’appliquent pour identifier des modules et packages par rapport aux fichiers correspondants:
- Un module est identifié par le nom du fichier sans l’extension
.py
: si le fichier s’appellehello_world.py
alors le module s’appellerahello_world
. - Un package est identifié de la même façon qu’un répertoire avec son nom.
- Un sub-package est identifié à partir de ses packages parents suivant son chemin: ainsi si le sub-package est dans le répertoire
numpy/polynomial
alors le nom du sub-package seranumpy.polynomial
. - Un module dans un package: par exemple si on considère un module sous la forme d’un fichier
.py
dans le répertoirenumpy/polynomial/chebyshev.py
alors l’identifiant du module seranumpy.polynomial.chebyshev
.
Fonctionnement du système d’import
Un import consiste à effectuer 2 opérations:
- La recherche d’un module nommé et
- L’attribution d’un nom à ce module, la portée de ce nom étant local.
Mécanisme d’import des modules
L’utilisation de l’instruction import
correspond à un appel à la fonction __import__()
puis à l’attribution d’un nom local aux modules importés. Cette attribution se fait en rajoutant une entrée dans le dictionnaire sys.module
. Ce dictionnaire permet de retrouver un module en fonction de son nom.
L’exécution de la fonction __import__()
déclenche une série d’étapes qui ont pour but de trouver le module à charger:
- Suivant le nom du module, une recherche est effectuée pour déterminer l’emplacement du module en fonction de son nom.
- Avant d’effectuer une recherche à proprement parlé, une recherche est faite en utilisant le dictionnaire
sys.modules
contenant les modules en fonction du nom. Si le module n’est pas trouvé dans ce dictionnaire, la recherche se poursuit. - La recherche est effectuée par des objets appelés finders ou des importers. D’autres objets appelés loaders permettent de charger les modules. Les importers sont à la fois des finders et des loaders.
Par défaut, Python contient un certain nombre de finders et importers en parcourant
sys.meta_path
. Tous les objets desys.meta_path
sont appelés successivement pour trouver le module.Pour voir le contenu du tableau
sys.meta_path
, il suffit d’exécuter:>>> import sys >>> sys.meta_path [_frozen_importlib.BuiltinImporter, _frozen_importlib.FrozenImporter, _frozen_importlib_external.PathFinder, <six._SixMetaPathImporter at 0xffff90cc1fa0>, <pkg_resources.extern.VendorImporter at 0xffff90a7ceb0>]
On peut voir que la recherche est effectuée sur les modules “built-in” c’est-à-dire des modules écrit en C faisant partie du shell Python. Dans un 2e temps, la recherche s’effectue pour des modules “frozen”. Les modules “frozen” sont des modules compilés en byte-code exécutables sans l’utilisation de Python. Dans 3e temps, une correspondance est cherchée entre le nom du module et son emplacement suivant le chemin d’import (i.e. import path). La liste des emplacements du chemin d’import peut être listée en affichant le contenu de
sys.path
(les chemins dépendent de la distribution Python, de l’environnement et du contexte d’exécution). 2 autres variables peuvent être utilisées pour trouver le chemin d’un modulesys.path_hooks
etsys.path_importer_cache
.L’emplacement d’un module n’est pas forcément sur le disque, il pourrait être dans une archive
.zip
ou accessible avec une URL.Lorsqu’un finder trouve la correspondance entre un module et son nom, il retourne le loader correspondant. Si le finder est un importer, il retourne sa propre instance.
- Le module est rajouté dans
sys.modules
avant son exécution. Si l’exécution du module échoue, il est retiré du dictionnairesys.modules
. L’ajout danssys.modules
est faite au préalable car l’exécution du module peut l’amener à se charger lui-même ce qui pourrait provoquer une boucle récursive infinie en le rajoutant indéfiniment danssys.modules
. - Le loader correspondant exécute le module (en appelant la méthode
exec_module()
). Les modules peuvent se trouver dans des packages. Un package est lui-même un module avec un attribut__path__
indiquant le chemin du package.Il existe 2 types de packages:
- Regular packages: ils sont implémentés dans un répertoire contenant un fichier
__init__.py
. Ce fichier est exécuté implicitement à chaque import de ce type de packages. Les sub-packages c’est-à-dire les répertoires enfant doivent aussi contenir des fichiers__init__.py
qui, en cas d’import, seront exécutés après le__init__.py
du répertoire parent. - Namespace packages: ces packages sont constitués de portions pouvant se trouver à des emplacements différents dans le système de fichier. Par exemple, une portion peut être un fichier sur le disque et un autre peut être à un emplacement sur le réseau.
- Regular packages: ils sont implémentés dans un répertoire contenant un fichier
Attributs des modules
Lors de l’import de modules, des objets correspondant sont créés pour accéder aux fonctions dans les modules. Le mécanisme d’import ajoute quelques attributs indiquant d’où provient le module:
__name__
: le nom du module__loader__
: nom du loader utilisé pour importer le module. Les finders et loaders disponibles peuvent être listés avecsys.meta_path
.__package__
: dans le cas d’un package cet attribut possède la même valeur que__name__
.__spec__
: ensemble des spécifications utilisées pour le système d’import.__path__
: chemin du package sur le disque. Ce chemin dépend de l’environnement virtuel utilisé.__file__
: fichier__init__.py
utilisé itinialement. Ce chemin dépend de l’environnement virtuel utilisé.__cached__
: fichier compilé (voir CPython) utilisé à l’exécution. Ce chemin dépend de l’environnement virtuel utilisé.
En utilisant la fonction dir()
, on peut voir la liste des attributs d’un module.
Chemin des modules dans les packages
Dans les packages, les imports de modules se font avec un chemin relatif au module courant ou en indiquant le chemin à partir du répertoire parent. Comme indiqué précédemment, les modules sont identifiés par nom. Dans le cas de modules présents sur le disque, le nom permet d’indiquer l’emplacement physique du fichier .py
correspondant. Le nom des modules à importer doit respecter les règles suivantes:
- Le nom ne doit pas comporter de
/
ou\
(à la différence d’un chemin de fichier), ils doivent être remplacés par.
. .
permet d’indiquer le répertoire courant...
permet d’indiquer le répertoire parent.- Le nom du module correspond au nom du fichier correspondant sans l’extension
.py
.
Ainsi si on veut indiquer le chemin de:
./Module1.py
alors l’import peut se faire avecfrom .Module1 import *
./InnerModule/Module3.py
alors l’import peut se faire avecfrom .InnerModule.Module3 import *
../Module2.py
l’import peut se faire avecfrom ..Module2 import *
Par exemple si on considère les fichiers suivants:
Hello
├── InnerModule
│ ├── __init__.py
│ └── Module3.py
├── __init__.py
├── Module1.py
├── Module2.py
└── setup.py
Le contenu des fichiers est:
Hello/Module1.py
:from Hello.Module2 import HelloFromModule2 def HelloFromModule1(): print("Hello from module 1") HelloFromModule2()
Hello/Module2.py
:from Hello.InnerModule.Module3 import HelloFromModule3 def HelloFromModule2(): print("Hello from module 2") HelloFromModule3()
Hello/InnerModule/Module3.py
:def HelloFromModule3(): print("Hello from module 3")
Dans ces exemples, les imports ont été faits suivant le répertoire parent du package. Si on avait indiqué les chemins de façon relative, il aurait fallu effectuer les imports de cette façon:
Hello/Module1.py
:from .Module2 import HelloFromModule2
Hello/Module2.py
:from .InnerModule.Module3 import HelloFromModule3
On peut voir les fichiers de cet exemple dans le repository GitHub: github.com/msoft/python_example_module_import.
Il est préférable de créer un environnement virtuel avant de continuer. Pour créer un environnement virtuel, on peut exécuter:
~/python_example_module_import% python -m venv venv
~/python_example_module_import% source venv/bin/activate
On peut ensuite installer les packages nécessaires pour construire le package:
(venv) ~/python_example_module_import/package_creation% pip install setuptools wheel
Pour construire le package .whl
, il faut exécuter dans le répertoire package_creation
:
(venv) ~/python_example_module_import/package_creation% python setup.py bdist_wheel
Le package sera créé dans le répertoire: python_example_module_import/package_creation/dist
.
Pour installer le package, il suffit d’exécuter:
(venv) ~/python_example_module_import/package_creation/dist% pip install Hello-1.0-py3-none-any.whl
Les fichiers du package se trouvent dans un répertoire du type: python_example_module_import/venv/lib/python3.9/site-packages/Hello
.
Enfin pour utiliser le package installé, on peut exécuter le fichier python_example_module_import/package_usage/test.py
:
(venv) ~/python_example_module_import/package_usage% python test.py
Hello from module 1
Hello from module 2
Hello from module 3
.whl
?Pour créer un package, on peut utiliser un fichier setup.py
et le package setuptools
, voir Construire un package wheel.
Encapsulation
L’encapsulation au niveau de la syntaxe du langage à proprement parlé n’existe pas en Python. Toutefois il est possible de ne pas exposer des modules lorsqu’ils sont dans des packages. En effet dans les fichiers __init__.py
des répertoires du packages, on peut indiquer les modules à importer lorsque le package est importé. Ainsi suivant les imports qui y sont effectués, on peut choisir d’exposer des modules particuliers ou de ne pas en exposer d’autres à l’extérieur.
Par exemple, si on considère l’exemple précédent, dans le fichier Hello/__init__.py
, si on ne souhaite exposer que le Module1
alors on peut effectuer l’import de cette façon:
from .Module1 import HelloFromModule1
Dans ce cas à l’extérieur du package, on pourra effectuer l’import de cette façon:
import Hello as h
h.HelloFromModule1()
Le module Module2
n’étant pas exposé dans le fichier Hello/__init__.py
, il n’est pas accessible de l’extérieur:
h.HelloFromModule2() # ERREUR
Pour que HelloFromModule2()
soit accessible, il faut l’importer en modifiant Hello/__init__.py
de cette façon:
from .Module1 import HelloFromModule1
from .Module2 import HelloFromModule2
Import d’un package
Comme indiqué précédemment:
- Les seuls objets Python sont les modules.
- Les packages sont des modules avec un attribut
__path__
.
Le système d’import a donc le même comportement qu’ils s’agissent de modules ou de packages. Il faut toutefois faire attention à la syntaxe utilisée lors de l’import. Ainsi les imports peuvent se faire:
- Relativement au fichier courant ou
- Par rapport au répertoire initial du package.
La recherche d’un module se fait en utilisant son nom (cf. Mécanisme d’import des modules). Le système d’import parcourt les répertoires du Python path pour trouver le module suivant son nom. Dans un premier temps, on va montrer comment récupérer les répertoires du Python path. Dans un 2e temps, on va indiquer la syntaxe d’import des modules et packages.
Python path
Le Python path est un tableau indiquant les chemins parcourus par le système d’import pour trouver un module suivant son nom. Pour obtenir cette liste de répertoire, il faut exécuter:
>>> import sys >>> sys.path
Le résultat dépend du système d’exploitation et de la distribution Python utilisé toutefois le tableau devrait contenir en particulier:
- Le répertoire courant,
- Le répertoire de l’exécutable python: par exemple
<répertoire d'Anaconda/lib/python3.9
. - Le répertoire des packages: par exemple
<répertoire d'Anaconda/lib/python3.9/site-packages
. - Le répertoire des packages dans le cas d’un environnement virtuel: par exemple
<répertoire env. virtuel/lib/python3.9/site-packages
.
On peut voir les modules déjà importés en exécutant:
>>> sys.modules
Import de modules
Un module possède un namespace privé et ce namespace n’est pas directement accessible à l’extérieur du module. Un module peut importer un autre module.
Plusieurs syntaxes sont possibles pour importer un module:
import <nom du module>
: le module est importé dans le namespace local toutefois tous les noms des objets ne sont pas accessibles à partir du namespace local. Pour accéder aux objets du module, il faut taper<nom du module>.<nom de l'objet>
.Par exemple:
import pandas data = pandas.DataFrame()
import <nom de l'objet> as <nom alias>
: permet d’éviter d’utiliser le nom entier du module pour accéder à ses objets. Avec cette syntaxe, le module est importé dans le namespace local toutefois les objets ne sont accessibles qu’en utilisant l’alias du module:<nom alias>.<nom de l'objet>
.Par exemple:
import pandas as pd data = pd.DataFrame()
from <nom du module> import <nom de l'objet>
: on ne charge qu’un seul objet du module dans le namespace local. Cet objet est accessible en utilisant directement son nom.Par exemple:
from pandas import DataFrame data = DataFrame()
from <nom du module> import *
: tous les noms des objets du module sont importés dans le namespace local. Il n’est pas recommandé d’utiliser cette syntaxe car il peut y avoir des collisions entre des modules qui utiliseraient les mêmes noms d’objet. Avec cette syntaxe, les objets sont accessibles directement par leur nom.Par exemple:
from pandas import * data = DataFrame()
from <nom du module> import <nom de l'objet> alias <alias de l'objet>
: cette syntaxe permet d’importer le nom d’un objet du module et de permettre d’utiliser cet objet en utilisant un alias.Par exemple:
from pandas import DataFrame as PandasDataframe data = PandasDataframe()
Avoir des informations sur un module importé
La fonction dir()
permet de lister les noms d’objets définis dans le namespace local. Cette fonction permet de lister les variables, les fonctions et les modules.
Ainsi:
dir()
: sans argument affiche les noms de variables, fonctions et modules qui sont accessibles dans le namespace local.dir(<nom du module>)
: liste les objets accessibles dans le module.
Par exemple, si on importe le package numpy:
>>> import numpy as np >>> dir(np) ['ALLOW_THREADS', 'AxisError', 'BUFSIZE', 'Bytes0', 'CLIP', 'ComplexWarning', 'DataSource', 'Datetime64', 'ERR_CALL', 'ERR_DEFAULT', 'ERR_IGNORE', 'ERR_LOG', 'ERR_PRINT', 'ERR_RAISE', 'ERR_WARN', 'FLOATING_POINT_SUPPORT', 'FPE_DIVIDEBYZERO', 'FPE_INVALID', 'FPE_OVERFLOW', 'FPE_UNDERFLOW', ..., 'True_', 'UFUNC_BUFSIZE_DEFAULT', 'UFUNC_PYVALS_NAME', 'Uint64', 'VisibleDeprecationWarning', 'WRAP', '_NoValue', '_UFUNC_API', '__NUMPY_SETUP__', '__all__', '__builtins__', '__cached__', '__config__', '__deprecated_attrs__', '__dir__', '__doc__', '__expired_functions__', '__file__', '__getattr__', '__git_revision__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_add_newdoc_ufunc', '_distributor_init', '_financial_names', '_globals', '_mat', '_pytesttester', 'abs', ..., 'fft', 'square', ..., 'uint', 'uint0', 'uint16', 'uint32', 'uint64', 'uint8', ..., 'where', 'who', 'zeros', 'zeros_like']
Ainsi parmi les objets listés, il peut y avoir:
- Des constantes:
>>> print(np.ERR_LOG) 5
- Des fonctions:
>>> print(np.sum) <function sum at 0xffff8c103160>
- Des modules:
>>> print(np.fft) <module 'numpy.fft' from ‘<chemin environnement virtuel>/site-packages/numpy/fft/__init__.py'>
- Des attributs:
>>> print(np.__package__) numpy
- Des classes:
>>> print(np.uint) <class 'numpy.uint64'>
- Rédaction du script setup.py: https://docs.python.org/fr/3/distutils/setupscript.html
- The import system: https://docs.python.org/3/reference/import.html
- Python Modules vs Packages: https://pythongeeks.org/python-modules-vs-packages/
- Python path: https://python.doctor/page-python-path-pythonpath
- setup() args: https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#setup-args
- A Practical Guide to Using Setup.py: https://godatadriven.com/blog/a-practical-guide-to-using-setup-py/
- Construire des extensions C et C++: https://docs.python.org/fr/3.10/extending/building.html
- What is a frozen Python module?: https://stackoverflow.com/questions/9916432/what-is-a-frozen-python-module