25. Import Système Avancé

importlib, import hooks, namespace packages, lazy loading, sys.meta_path, __path__, finders/loaders, pkgutil.


25.1 Rappel : comment fonctionne import

import mon_module
  1. Cherche dans sys.modules (cache)
  2. Si absent, parcourt sys.meta_path (finders)
  3. Le finder trouve le loader, qui exécute le module
  4. Stocke dans sys.modules
import sys
sys.modules["math"]  # <module 'math' ...> (déjà en cache)

25.2 sys.meta_path — les importateurs

import sys
for finder in sys.meta_path:
    print(finder)
# <class '_frozen_importlib.BuiltinImporter'>
# <class '_frozen_importlib.FrozenImporter'>
# <class '_frozen_importlib_external.PathFinder'>
  • BuiltinImporter : modules compilés en C (ex: sys, math)
  • FrozenImporter : modules gelés dans l’exécutable
  • PathFinder : parcourt sys.path pour les fichiers .py/.so

25.3 Import dynamique avec importlib

import importlib
 
# Import par nom (chaîne)
math = importlib.import_module("math")
math.sqrt(16)  # 4.0
 
# Import de sous-module
os_path = importlib.import_module("os.path")

Équivalent à :

import math  # connu à la compilation

Avantage : le nom du module peut être une variable.

nom_module = "numpy"
np = importlib.import_module(nom_module)

importlib.util.spec_from_file_location

Importer un fichier spécifique :

import importlib.util
 
spec = importlib.util.spec_from_file_location("mon_module", "/chemin/vers/module.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.ma_fonction()

25.4 __import__ — la fonction native

# Niveau le plus bas
math = __import__("math")
# Équivaut à import math
 
# Import hiérarchique
os = __import__("os.path")
# os = sys.modules["os"] (pas os.path)

Déconseillé : préférer importlib.import_module.

25.5 Finders et Loaders personnalisés

Charger des modules depuis une source non standard (base de données, réseau, etc.) :

import sys
import importlib.abc
import importlib.machinery
 
class DatabaseFinder(importlib.abc.MetaPathFinder):
    """Trouve des modules stockés en base de données."""
 
    def find_spec(self, fullname, path, target=None):
        if not fullname.startswith("db_"):
            return None
        return importlib.machinery.ModuleSpec(
            fullname,
            DatabaseLoader(fullname),
        )
 
class DatabaseLoader(importlib.abc.Loader):
    def __init__(self, fullname):
        self.fullname = fullname
 
    def create_module(self, spec):
        return None  # utilisation du mécanisme par défaut
 
    def exec_module(self, module):
        # Charger le code depuis la base de données
        code = """
def hello():
    print("Bonjour depuis la DB !")
"""
        exec(code, module.__dict__)
        module.__loader__ = self
 
# Enregistrer le finder AVANT les imports
sys.meta_path.insert(0, DatabaseFinder())
 
import db_hello
db_hello.hello()  # "Bonjour depuis la DB !"

25.6 Path hooks — sys.path_hooks

Pour des protocoles personnalisés :

# Exemple: importer depuis une archive zip
import zipimport
import sys
 
# zipimport est déjà dans sys.path_hooks par défaut
# On peut ajouter un zip directement dans sys.path
sys.path.append("/chemin/vers/archive.zip")
 
# Les modules dans l'archive sont importables

Créer un path hook personnalisé

import sys
import importlib.abc
 
class MyPathFinder(importlib.abc.PathEntryFinder):
    def __init__(self, path):
        self.path = path
 
    def find_spec(self, fullname, target=None):
        ...
 
def path_hook(path):
    if path.startswith("custom://"):
        return MyPathFinder(path)
    raise ImportError
 
sys.path_hooks.append(path_hook)

25.7 Reload — recharger un module

import importlib
import mon_module
 
# Modifier mon_module...
 
importlib.reload(mon_module)  # recharger

Attention : reload ne met pas à jour les références existantes (from mon_module import f reste l’ancienne fonction).

25.8 Namespace packages

Paquets sans __init__.py, qui peuvent être répartis sur plusieurs dossiers :

projet/
├── paquet/
│   └── module_a.py
autre_chemin/
├── paquet/
│   └── module_b.py
import sys
sys.path.extend(["projet", "autre_chemin"])
 
import paquet.module_a  # de projet/paquet/
import paquet.module_b  # de autre_chemin/paquet/

Création :

# pas de __init__.py dans les dossiers paquet/
# Python 3.3+ les détecte automatiquement comme namespace packages

Cas d’usage : plugins, extensions, séparation de codebase.

25.9 Lazy loading

Importer seulement quand nécessaire :

import importlib.util
import sys
 
class LazyLoader:
    def __init__(self, module_name):
        self.module_name = module_name
        self._module = None
 
    def __getattr__(self, name):
        if self._module is None:
            self._module = importlib.import_module(self.module_name)
        return getattr(self._module, name)
 
numpy = LazyLoader("numpy")
# numpy n'est importé que lors du premier accès
numpy.array([1, 2, 3])  # import ici

Approche standard : importlib.util.LazyLoader (stdlib, 3.5+)

import importlib.util
import importlib.abc
 
spec = importlib.util.spec_from_loader("lazy_math", None)
lazy_math = importlib.util.module_from_spec(spec)
spec.loader = importlib.util.LazyLoader(spec.loader)
sys.modules["lazy_math"] = lazy_math

25.10 pkgutil — exploration de paquets

import pkgutil
 
# Lister les sous-modules d'un paquet
modules = list(pkgutil.iter_modules(["chemin/vers/paquet"]))
for finder, name, ispkg in modules:
    print(name, ispkg)
 
# Importer tous les sous-modules
for finder, name, ispkg in pkgutil.walk_packages(["paquet"]):
    importlib.import_module(f"paquet.{name}")

Utile pour les plugins auto-découverts :

# plugins/__init__.py
import importlib
import pkgutil
 
def charger_plugins():
    plugins = {}
    for finder, name, ispkg in pkgutil.iter_modules(__path__):
        module = importlib.import_module(f"plugins.{name}")
        if hasattr(module, "Plugin"):
            plugins[name] = module.Plugin
    return plugins

25.11 sys.modules — manipulation directe

import sys
 
# Ajouter un module factice
class FauxModule:
    def fonction(self):
        return "factice"
 
sys.modules["faux"] = FauxModule()
import faux
faux.fonction()  # "factice"
# Retirer un module (forcer le réimport)
if "mon_module" in sys.modules:
    del sys.modules["mon_module"]
import mon_module  # réimporté

25.12 __init__.py avancé

# Dans mon_paquet/__init__.py
from .module_a import ClasseImportante
from .sous_paquet import fonction_utile
 
__all__ = ["ClasseImportante", "fonction_utile"]
 
# Initialisation à l'import du paquet
print("Paquet initialisé")

__path__ :

# Dans __init__.py
print(__path__)  # list[str] des chemins du paquet
__path__.append("/autre/chemin")  # ajouter des sous-modules dynamiquement

25.13 Bonnes pratiques

  • importlib.import_module plutôt que __import__
  • reload utile en développement interactif (IPython %autoreload)
  • Namespace packages : uniquement quand nécessaire (plugins distribués)
  • Lazy loading : utile pour les imports coûteux conditionnels
  • Ne pas abuser des finders personnalisés

🔗 ← Retour au cours · ← précédent · Suivant →