Python __init__.py, plus qu'un simple fichier vide



De nombreux développeurs Python utilisent le fichier __init__.py uniquement comme un fichier vide pour « faire reconnaître ce répertoire comme un package ». Cependant, ce fichier n'est pas un simple marqueur.

Le cœur de __init__.py est que c'est le « script qui s'exécute en premier lorsque le package est importé ».

En profitant de cette caractéristique, vous pouvez effectuer des tâches très puissantes, comme nettoyer du code en désordre, concevoir clairement l'API du package et gérer la logique d'initialisation.


1. Rendre les chemins d'importation encombrants plus clairs (Simplification de l'API)

C'est la manière la plus utile et commune d'utiliser __init__.py. Il permet de « remonter » les classes ou fonctions des sous-modules au niveau du package, ce qui facilite leur importation.

Problème :

Supposons que le views.py de Django dépasse 1 000 lignes et que les fichiers soient séparés par fonctionnalité.

payments/
├── views/
│   ├── __init__.py
│   ├── user_views.py    # UserProfileView, ...
│   ├── payment_views.py # PaymentView, ...
│   └── webhooks.py      # PaypalWebhookView, ...
└── urls.py

Il est très fastidieux d'utiliser ces vues dans le urls.py.

# payments/urls.py (mauvais exemple)
from .views import user_views, payment_views, webhooks

urlpatterns = [
    path('profile/', user_views.UserProfileView.as_view()),
    path('toss/', payment_views.PaymentView.as_view()),
    path('webhook/', webhooks.PaypalWebhookView.as_view()),
]

Le urls.py doit connaître la structure interne des fichiers du répertoire views.


Solution (Utilisation de init.py) :

Vous importez à l'avance les vues nécessaires dans le fichier views/init.py.

# payments/views/__init__.py

# Remontez les classes/fonctions nécessaires au niveau du package.
from .user_views import UserProfileView
from .payment_views import PaymentView
from .webhooks import PaypalWebhookView

Maintenant, le urls.py utilisant le package views a l'air de posséder UserProfileView directement.

# payments/urls.py (exemple amélioré)

# 'views' est désormais un package (répertoire) plutôt qu'un simple fichier.
from . import views

urlpatterns = [
    # Grâce à __init__.py, il n'est plus nécessaire de connaître la structure interne.
    path('profile/', views.UserProfileView.as_view()),
    path('toss/', views.PaymentView.as_view()),
    path('webhook/', views.PaypalWebhookView.as_view()),
]

Que views.py soit un fichier ou un répertoire contenant plusieurs fichiers, le code dans urls.py reste le même. C'est ce qu'on appelle une bonne abstraction.


2. Définir la 'API officielle' du package (__all__)



from my_package import * (importation par wildcard) est pratique, mais cela peut également amener des variables ou des modules non souhaités (par exemple, import os) dans l'espace de noms actuel, le polluant.

__all__ définit une liste blanche qui permet uniquement d'importer les éléments figurant dans cette liste lorsque vous utilisez import *.

# payments/views/__init__.py

# Modules pour usage interne
import logging 
from .internal_utils import _helper_function

# Vues à rendre publiques
from .user_views import UserProfileView
from .payment_views import PaymentView

# Définition de __all__:
# "Lorsque vous utilisez import *, seules UserProfileView et PaymentView peuvent être importées."
__all__ = [
    'UserProfileView',
    'PaymentView'
]

Désormais, même si vous exécutez from payments.views import *, le module logging ou la fonction interne _helper_function ne seront pas importés, protégeant ainsi l'espace de noms.


3. Initialisation du package et définition des métadonnées

C'est un endroit parfait pour placer le code qui doit être exécuté une seule fois lors du chargement du package.

  • Fournir des informations sur la version :
# my_library/__init__.py
__version__ = "1.2.0"
__author__ = "whitedec"

Désormais, l'utilisateur peut facilement vérifier la version en exécutant import my_library; print(my_library.__version__).

  • Exécution d'un réglage global :

Il est courant que Django appelle inner django.setup() lors de import django, ou que du code pour initialiser une application dans Celery soit inclus dans init.py.


# my_project/__init__.py (exemple de Celery)

# Garantir que l'application Celery est chargée avec Django
from .celery import app as celery_app

__all__ = ('celery_app',)


※ Attention : Références circulaires entre modules internes

Lorsque vous utilisez __init__.py comme un 'bureau d'accueil', il y a quelques précautions à prendre.

Supposons que views/webhooks.py doive utiliser CancelView de views/payment_commons.py.

  • Mauvaise méthode (risque de référence circulaire) :

Tenter d'importer dans webhooks.py via from . import CancelView (c'est-à-dire init.py).

  • Bonne méthode (import direct) :

Importer directement le module nécessaire dans webhooks.py via from .payment_commons import CancelView.

Règles :

  • Utilisateurs externes (par exemple, urls.py) : utilisent l'API via le 'bureau d'accueil' (__init__.py).
  • Modules internes (par exemple, webhooks.py) : importent directement les fonctionnalités d'autres modules sans passer par le 'bureau d'accueil'.

Conclusion

Le __init__.py n'est pas un simple fichier vide, mais un outil puissant qui peut servir de 'bureau d'accueil', 'documentation API officielle', 'script de configuration initiale' d'un package. En l'utilisant bien, vous pouvez rendre la structure de votre projet beaucoup plus propre et plus facile à maintenir.