Importations circulaires et dynamiques de modules

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Dans ce laboratoire, vous allez apprendre deux concepts essentiels liés aux importations en Python. Les importations de modules en Python peuvent parfois entraîner des dépendances complexes, conduisant à des erreurs ou à des structures de code inefficaces. Les importations circulaires, où deux modules ou plus s'importent mutuellement, créent une boucle de dépendance qui peut causer des problèmes si elle n'est pas correctement gérée.

Vous allez également explorer les importations dynamiques, qui permettent de charger des modules à l'exécution plutôt qu'au démarrage du programme. Cela offre de la flexibilité et aide à éviter les problèmes liés aux importations. Les objectifs de ce laboratoire sont de comprendre les problèmes d'importation circulaire, de mettre en œuvre des solutions pour les éviter et d'apprendre à utiliser efficacement les importations dynamiques de modules.

Comprendre le problème d'importation

Commençons par comprendre ce que sont les importations de modules. En Python, lorsque vous souhaitez utiliser des fonctions, des classes ou des variables provenant d'un autre fichier (module), vous utilisez l'instruction import. Cependant, la manière dont vous structurez vos importations peut entraîner divers problèmes.

Maintenant, nous allons examiner un exemple de structure de module problématique. Le code dans tableformat/formatter.py contient des importations réparties dans tout le fichier. Cela peut ne pas sembler être un gros problème au premier abord, mais cela crée des problèmes de maintenance et de dépendance.

Tout d'abord, ouvrez l'explorateur de fichiers de l'IDE Web et accédez au répertoire structly. Nous allons exécuter quelques commandes pour comprendre la structure actuelle du projet. La commande cd est utilisée pour changer le répertoire de travail actuel, et la commande ls -la liste tous les fichiers et répertoires dans le répertoire actuel, y compris les fichiers cachés.

cd ~/project/structly
ls -la

Cela vous montrera les fichiers dans le répertoire du projet. Maintenant, nous allons examiner l'un des fichiers problématiques en utilisant la commande cat, qui affiche le contenu d'un fichier.

cat tableformat/formatter.py

Vous devriez voir un code similaire au suivant :

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

def create_formatter(name, column_formats=None, upper_headers=False):
    if name == 'text':
        formatter_cls = TextTableFormatter
    elif name == 'csv':
        formatter_cls = CSVTableFormatter
    elif name == 'html':
        formatter_cls = HTMLTableFormatter
    else:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Remarquez l'emplacement des instructions d'importation au milieu du fichier. Cela pose problème pour plusieurs raisons :

  1. Cela rend le code plus difficile à lire et à maintenir. Lorsque vous regardez un fichier, vous vous attendez à voir toutes les importations au début afin de pouvoir rapidement comprendre quels modules externes le fichier dépend.
  2. Cela peut entraîner des problèmes d'importation circulaire. Les importations circulaires se produisent lorsque deux modules ou plus dépendent les uns des autres, ce qui peut causer des erreurs et faire que votre code se comporte de manière inattendue.
  3. Cela viole la convention Python consistant à placer toutes les importations en haut d'un fichier. Suivre les conventions rend votre code plus lisible et plus facile à comprendre pour les autres développeurs.

Dans les étapes suivantes, nous explorerons ces problèmes plus en détail et apprendrons à les résoudre.

Explorer les importations circulaires

Une importation circulaire est une situation où deux modules ou plus dépendent les uns des autres. Plus précisément, lorsque le module A importe le module B, et que le module B importe également le module A, soit directement, soit indirectement. Cela crée une boucle de dépendance que le système d'importation de Python ne peut pas résoudre correctement. En termes plus simples, Python reste bloqué dans une boucle en essayant de déterminer quel module importer en premier, ce qui peut entraîner des erreurs dans votre programme.

Expérimentons avec notre code pour voir comment les importations circulaires peuvent causer des problèmes.

Tout d'abord, nous allons exécuter le programme de gestion des stocks pour vérifier s'il fonctionne avec la structure actuelle. Cette étape nous permet d'établir une référence et de voir le programme fonctionner comme prévu avant d'apporter des modifications.

cd ~/project/structly
python3 stock.py

Le programme devrait s'exécuter correctement et afficher les données des stocks dans un tableau formaté. Si c'est le cas, cela signifie que la structure actuelle du code fonctionne bien sans aucun problème d'importation circulaire.

Maintenant, nous allons modifier le fichier formatter.py. En général, il est recommandé de déplacer les importations en haut d'un fichier. Cela rend le code plus organisé et plus facile à comprendre d'un coup d'œil.

cd ~/project/structly

Ouvrez tableformat/formatter.py dans l'IDE Web. Nous allons déplacer les importations suivantes en haut du fichier, juste après les importations existantes. Ces importations concernent différents formatteurs de tableaux, comme le format texte, CSV et HTML.

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Le début du fichier devrait maintenant ressembler à ceci :

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

Enregistrez le fichier et essayez d'exécuter à nouveau le programme de gestion des stocks.

python3 stock.py

Vous devriez voir un message d'erreur indiquant que TableFormatter n'est pas défini. Ceci est un signe clair d'un problème d'importation circulaire.

Le problème se produit en raison de la chaîne d'événements suivante :

  1. formatter.py essaie d'importer TextTableFormatter depuis formats/text.py.
  2. formats/text.py importe TableFormatter depuis formatter.py.
  3. Lorsque Python essaie de résoudre ces importations, il reste bloqué dans une boucle car il ne peut pas décider quel module importer entièrement en premier.

Revenons en arrière sur nos modifications pour que le programme fonctionne à nouveau. Modifiez tableformat/formatter.py et replacez les importations à leur emplacement d'origine (après la définition de la classe TableFormatter).

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Exécutez le programme à nouveau pour confirmer qu'il fonctionne.

python3 stock.py

Cela démontre que même si avoir des importations au milieu du fichier n'est pas la meilleure pratique en termes d'organisation du code, cela a été fait pour éviter un problème d'importation circulaire. Dans les étapes suivantes, nous explorerons de meilleures solutions.

Mise en œuvre de l'enregistrement des sous-classes

En programmation, les importations circulaires peuvent être un problème délicat. Au lieu d'importer directement les classes de formatage, nous pouvons utiliser un modèle d'enregistrement. Dans ce modèle, les sous-classes s'enregistrent auprès de leur classe mère. C'est une méthode courante et efficace pour éviter les importations circulaires.

Tout d'abord, comprenons comment nous pouvons trouver le nom du module d'une classe. Le nom du module est important car nous l'utiliserons dans notre modèle d'enregistrement. Pour ce faire, nous allons exécuter une commande Python dans le terminal.

cd ~/project/structly
python3 -c "from structly.tableformat.formats.text import TextTableFormatter; print(TextTableFormatter.__module__); print(TextTableFormatter.__module__.split('.')[-1])"

Lorsque vous exécutez cette commande, vous verrez une sortie comme celle-ci :

structly.tableformat.formats.text
text

Cette sortie montre que nous pouvons extraire le nom du module à partir de la classe elle-même. Nous utiliserons ce nom de module plus tard pour enregistrer les sous-classes.

Maintenant, modifions la classe TableFormatter dans le fichier tableformat/formatter.py pour ajouter un mécanisme d'enregistrement. Ouvrez ce fichier dans l'IDE Web. Nous allons ajouter du code à la classe TableFormatter. Ce code nous aidera à enregistrer automatiquement les sous-classes.

class TableFormatter(ABC):
    _formats = { }  ## Dictionnaire pour stocker les formatteurs enregistrés

    @classmethod
    def __init_subclass__(cls):
        name = cls.__module__.split('.')[-1]
        TableFormatter._formats[name] = cls

    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

La méthode __init_subclass__ est une méthode spéciale en Python. Elle est appelée chaque fois qu'une sous-classe de TableFormatter est créée. Dans cette méthode, nous extrayons le nom du module de la sous-classe et l'utilisons comme clé pour enregistrer la sous-classe dans le dictionnaire _formats.

Ensuite, nous devons modifier la fonction create_formatter pour utiliser le dictionnaire d'enregistrement. Cette fonction est chargée de créer le formatteur approprié en fonction du nom donné.

def create_formatter(name, column_formats=None, upper_headers=False):
    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Après avoir apporté ces modifications, enregistrez le fichier. Ensuite, testons si le programme fonctionne toujours. Nous allons exécuter le script stock.py.

python3 stock.py

Si le programme s'exécute correctement, cela signifie que nos modifications n'ont rien cassé. Maintenant, regardons le contenu du dictionnaire _formats pour voir comment l'enregistrement fonctionne.

python3 -c "from structly.tableformat.formatter import TableFormatter; print(TableFormatter._formats)"

Vous devriez voir une sortie comme celle-ci :

{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>, 'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>, 'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}

Cette sortie confirme que nos sous-classes sont correctement enregistrées dans le dictionnaire _formats. Cependant, nous avons toujours des importations au milieu du fichier. Dans l'étape suivante, nous résoudrons ce problème en utilisant des importations dynamiques.

Utilisation des importations dynamiques

En programmation, les importations servent à inclure le code d'autres modules afin que nous puissions utiliser leur fonctionnalité. Cependant, parfois, avoir des importations au milieu d'un fichier peut rendre le code un peu désordonné et difficile à comprendre. Dans cette partie, nous allons apprendre à utiliser les importations dynamiques pour résoudre ce problème. Les importations dynamiques sont une fonctionnalité puissante qui nous permet de charger des modules à l'exécution, ce qui signifie que nous chargeons un module seulement lorsque nous en avons réellement besoin.

Tout d'abord, nous devons supprimer les instructions d'importation actuellement placées après la classe TableFormatter. Ces importations sont des importations statiques, qui sont chargées lorsque le programme démarre. Pour ce faire, ouvrez le fichier tableformat/formatter.py dans l'IDE Web. Une fois le fichier ouvert, recherchez et supprimez les lignes suivantes :

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Si vous essayez d'exécuter le programme maintenant en exécutant la commande suivante dans le terminal :

python3 stock.py

Le programme échouera. La raison en est que les formatteurs ne seront pas enregistrés dans le dictionnaire _formats. Vous verrez un message d'erreur concernant un format inconnu. C'est parce que le programme ne peut pas trouver les classes de formatteurs dont il a besoin pour fonctionner correctement.

Pour résoudre ce problème, nous allons modifier la fonction create_formatter. L'objectif est d'importer dynamiquement le module requis lorsqu'il est nécessaire. Mettez à jour la fonction comme indiqué ci-dessous :

def create_formatter(name, column_formats=None, upper_headers=False):
    if name not in TableFormatter._formats:
        __import__(f'{__package__}.formats.{name}')

    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

La ligne la plus importante de cette fonction est :

__import__(f'{__package__}.formats.{name}')

Cette ligne importe dynamiquement le module en fonction du nom du format. Lorsque le module est importé, sa sous-classe de TableFormatter s'enregistre automatiquement. Cela est dû à la méthode __init_subclass__ que nous avons ajoutée précédemment. Cette méthode est une méthode spéciale de Python qui est appelée lorsqu'une sous-classe est créée, et dans notre cas, elle est utilisée pour enregistrer la classe de formatteur.

Après avoir apporté ces modifications, enregistrez le fichier. Ensuite, exécutez le programme à nouveau en utilisant la commande suivante :

python3 stock.py

Le programme devrait maintenant fonctionner correctement, même si nous avons supprimé les importations statiques. Pour vérifier que l'importation dynamique fonctionne comme prévu, nous allons vider le dictionnaire _formats puis appeler la fonction create_formatter. Exécutez la commande suivante dans le terminal :

python3 -c "from structly.tableformat.formatter import TableFormatter, create_formatter; TableFormatter._formats.clear(); print('Before:', TableFormatter._formats); create_formatter('text'); print('After:', TableFormatter._formats)"

Vous devriez voir une sortie similaire à celle-ci :

Before: {}
After: {'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>}

Cette sortie confirme que l'importation dynamique charge le module et enregistre la classe de formatteur lorsque cela est nécessaire.

En utilisant les importations dynamiques et l'enregistrement des classes, nous avons créé une structure de code plus propre et plus facilement maintenable. Voici les avantages :

  1. Toutes les importations sont maintenant en haut du fichier, ce qui suit les conventions de Python. Cela rend le code plus facile à lire et à comprendre.
  2. Nous avons éliminé les importations circulaires. Les importations circulaires peuvent causer des problèmes dans un programme, tels que des boucles infinies ou des erreurs difficiles à déboguer.
  3. Le code est plus flexible. Maintenant, nous pouvons ajouter de nouveaux formatteurs sans modifier la fonction create_formatter. Cela est très utile dans un scénario réel où de nouvelles fonctionnalités pourraient être ajoutées au fil du temps.

Ce modèle d'utilisation des importations dynamiques et de l'enregistrement des classes est couramment utilisé dans les systèmes de plugins et les frameworks. Dans ces systèmes, les composants doivent être chargés dynamiquement en fonction des besoins de l'utilisateur ou des exigences du programme.

Résumé

Dans ce laboratoire (lab), vous avez appris des concepts et des techniques cruciales concernant les importations de modules Python. Tout d'abord, vous avez exploré les importations circulaires, en comprenant comment les dépendances circulaires entre les modules peuvent entraîner des problèmes et pourquoi il est nécessaire de les gérer avec soin pour les éviter. Deuxièmement, vous avez mis en œuvre l'enregistrement des sous-classes, un modèle dans lequel les sous-classes s'enregistrent auprès de leur classe mère, éliminant ainsi le besoin d'importer directement les sous-classes.

Vous avez également utilisé la fonction __import__() pour les importations dynamiques, chargeant les modules à l'exécution seulement lorsqu'ils sont nécessaires. Cela rend le code plus flexible et aide à éviter les dépendances circulaires. Ces techniques sont essentielles pour créer des packages Python maintenables avec des relations de modules complexes et sont couramment utilisées dans les frameworks et les bibliothèques. Appliquer ces modèles à vos projets peut vous aider à construire des structures de code plus modulaires, extensibles et maintenables.