Système d’import de modules Python


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é.

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’appelle hello_world.py alors le module s’appellera hello_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 sera numpy.polynomial.
  • Un module dans un package: par exemple si on considère un module sous la forme d’un fichier .py dans le répertoire numpy/polynomial/chebyshev.py alors l’identifiant du module sera numpy.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:

  1. Suivant le nom du module, une recherche est effectuée pour déterminer l’emplacement du module en fonction de son nom.
  2. 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.
  3. 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 de sys.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 module sys.path_hooks et sys.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.

  4. Le module est rajouté dans sys.modules avant son exécution. Si l’exécution du module échoue, il est retiré du dictionnaire sys.modules. L’ajout dans sys.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 dans sys.modules.
  5. 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.

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 avec sys.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 avec from .Module1 import *
  • ./InnerModule/Module3.py alors l’import peut se faire avec from .InnerModule.Module3 import *
  • ../Module2.py l’import peut se faire avec from ..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
    
Code source et exécution

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

Comment créer un package .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'>
    

Leave a Reply