Nous abordons ici le processus d'importation des modules, c'est-à-dire les différentes étapes que va suivre l'interpréteur Python du moment où on tape l'instruction *import* jusqu'au moment où l'objet module est disponible.
Lorsque l'on importe un module, on utilise l'instruction import, par exemple:
# Importation du module os >>> import os # Après interprétation de cette instruction, une variable # os est disponible elle référence un objet de type module >>> print(os) <module 'os' from '/usr/lib/python3.8/os.py'>
Le mot après l'instruction import
, ici os
a deux rôles:
os.py
( la plupart des modules sont écrits en python et ont une extension “.py” à quelques exceptions près directement écrits en *C*).os
définit également le nom de la variable qui va référencer l'objet module.Premièrement, il faut trouver le fichier sur le disque dur. Pour cela, Python va regarder dans un certain nombre de répertoires.
Ensuite, s'il ne trouve pas ce fichier, il va le chercher dans la variable système qui s'appelle `PYTHONPATH`.
Le module *os*, inclus un dictionnaire nommé *environ* (pour environnement), qui contient toutes les variables d'environnement du système sur lequel le programme s'exécute. On peut accéder à la valeur de cette variable `PYTHONPATH`:
>>> print(os.environ['PYTHONPATH'])
si le fichier n'est pas présent, au final, l’interpréteur va le chercher dans le répertoire des librairies standards. C'est pour ça qu'on peut importer n'importe quel module de la librairie standard sans avoir à se soucier de l'endroit où se situe ce fichier module.
Lorsque l'on a un doute sur le chemin de recherche, en fait, on peut regarder dans une variable qui s'appelle `sys.path`. Donc importons le module `sys` et `sys.path` est une liste qui contient tous les chemins qui sont suivis par l'interpréteur Python dans l'ordre, du premier chemin au dernier.
>>> import sys >>> print(sys.path) ['/usr/bin', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages', '/usr/lib/python3/dist-packages/IPython/extensions', '/home/yoann/.ipython']
Cette variable étant une liste, on peut la modifier en cours d'exécution et lorsque l'on fera une importation, le processus d'importation regardera l'état actuel de cette variable.
C'est ce qui nous permet, dans un programme, de pouvoir adapter les chemins de recherche des modules.
A présent que le fichier source du module a été identifié, l'interpréteur va devoir le pré-compiler ; la pré-compilation consiste à générer ce qu'on appelle du *bytecode*. Ce *bytecode* est stocké dans des fichiers d' extension .pyc
. Tous ces fichiers bytecode vont être mis dans un répertoire qui s'appelle pycache
. Ce répertoire est en général là où vous exécutez votre programme.
Pour finir, l'interpréteur Python, une fois qu'il a généré le *bytecode*, va évaluer ce *bytecode* pour générer l'objet module. Je vous rappelle qu'un module s'importe toujours de manière séquentielle donc on va parcourir les lignes de la première ligne à la dernière ligne de code dans l'ordre, du début jusqu'à la fin, et que lorsque l'on rencontre une fonction, on va créer les objets fonction ; par contre, le bloc de code de la fonction ne sera évalué qu'à l'appel de la fonction.
Le processus d'importation étant une opération coûteuse, l'interpréteur, lorsque vous faites de multiples *import* vers le même module, ne va importer ce module qu'une seule fois, et il va ensuite créer des références partagées vers cet objet module. C'est pourquoi il est très important de comprendre qu'un objet module est mutable, et que la manière d'importer un module peut avoir un impact sur l'espace de nommage de ce module ou l'espace de nommage de votre programme.
import
est une instruction comme une autre, et vous trouverez occasionnellement un avantage à l'utiliser à l'intérieur d'une fonction, sans aucun surcoût puisque vous ne payez le prix de l'import qu'au premier appel et non à chaque appel de la fonction.
>>> def ma_fonction(): ... import un_module_improbable ... ....
Cet usage n'est pas recommandé en général, mais peut s'avérer très pratique pour alléger les dépendances entre modules dans des contextes particuliers, comme du code multi-plateformes.
Cette stratégie de chargement unique des modules est appliquée par défaut mais python fournit dans le module importlib une fonction reload, qui permet comme son nom l'indique de forcer le rechargement d'un module, comme ceci:
>>> from importlib import reload >>> reload(multiple_import)
importlib.reload
est une fonction et non une instruction comme import: d'où cette syntaxe avec des parenthèses qui n'est pas celle de import.
L'interpréteur utilise la variable sys.modules
pour conserver la trace des modules actuellement chargés.
# vérifie si un module est chargé >>> 'this' in sys.modules True # les appels successif d'import ne produisent pas # d'affichage car le module est déjà chargé >>> import this >>> import this # On peut forcer un rechargement de module si on retire le module # du dictionnaire sys.module >>> del sys.modules['this'] # l'import suivant produit un affichage, le module est bien rechargé >>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. ...
Signalons enfin la variable sys.builtin_module_names
qui contient le nom des modules, comme par exemple le garbage collector gc, qui sont implémentés en C et font partie intégrante de l'interpréteur.
>>> 'gc' in sys.builtin_module_names True