Question 1Quel fichier dois-tu placer dans un dossier pour qu'il soit reconnu comme un package ?
__init__.py et imports relatifs — Regrouper des fichiers en package
Apprends le rôle de __init__.py pour construire des packages Python et quand recourir aux imports absolus vs relatifs.
L'article précédent couvrait les modules sur un seul fichier et le fonctionnement de import. Une fois qu'une application grandit, tu voudras regrouper les modules liés sous un seul dossier en tant que package. Cet article parcourt comment importer des packages.
Un package est un dossier contenant __init__.py
Un package est un dossier qui contient un fichier spécial appelé __init__.py. Quand Python le trouve, le dossier entier devient un seul package et d'autre code peut le récupérer avec import nom_dossier. __init__.py est la première chose qui s'exécute quand le package est importé, et c'est un endroit courant pour déclarer quelles fonctions et classes le package expose.
- __init__.py — le fichier qui s'exécute en premier quand le package est importé
- calculation.py — un module (add / multiply)
- string_utils.py — un module (format_name, etc.)
my_package/ contient __init__.py, donc Python traite le dossier comme un package. Les modules internes comme calculation.py sont exposés à l'extérieur via __init__.py.__init__.py peut être vide. Même un fichier vide suffit — Python a juste besoin de la présence du fichier pour reconnaître le dossier comme un package.
Importer sans passer par __init__.py
Même avec un __init__.py vide, tu peux toujours importer un module directement depuis le package. La forme est from nom_package.nom_module import nom_fonction — relie l'emplacement du fichier avec des points.
# my_package/__init__.py <- vide, ça va
# my_package/calculation.py
def add(a, b):
return a + b
# Dans main.py
from my_package.calculation import add # précise le nom de fichier `calculation`
print(add(1, 2)) # 3
L'inconvénient : les appelants doivent savoir quel fichier contient quoi. Si tu réorganises le package plus tard (disons, scinder calculation.py en deux), chaque appelant doit changer aussi. Le motif de la prochaine section consistant à ré-exporter via __init__.py corrige ça — il permet aux appelants de référencer des noms courts directement attachés au package.
Regrouper l'API publique dans __init__.py
Écris from .nom_module import nom_fonction à l'intérieur de __init__.py et la fonction apparaît comme si elle vivait directement sous le package. Par exemple, from .calculation import add, multiply dans __init__.py permet aux appelants d'écrire from my_package import add, multiply — court et propre.
from my_package import add sans se soucier de quel fichier (calculation) le contient.Pourquoi regrouper l'API publique dans __init__.py
Si les appelants ne touchent qu'à ce que __init__.py expose, tu peux remanier les fichiers internes plus tard sans toucher au code des appelants. C'est de l'encapsulation classique au niveau du package — le motif de base du design de package.
from vs import — Deux façons de l'écrire
Il existe deux formes de import : from package import nom et import package. Les deux récupèrent la cible, mais la façon dont tu appelles ensuite est différente.
from package import nom fait entrer le nom lui-même dans la portée pour que tu puisses l'utiliser directement. import package ne fait entrer que le nom du package dans la portée ; les appels doivent utiliser le chemin complet pointé.# Forme 1 : from ... import ...
from my_package.calculation import add
print(add(1, 2)) # add est directement utilisable
# Forme 2 : import ...
import my_package.calculation
print(my_package.calculation.add(1, 2)) # appel avec le chemin pointé complet
# Forme 2 + alias as (raccourcit les noms longs)
import my_package.calculation as calc
print(calc.add(1, 2))
| Forme | Nom dans la portée | Comment appeler |
|---|---|---|
| from my_package.calculation import add | add | add(1, 2) |
| import my_package.calculation | my_package | my_package.calculation.add(1, 2) |
| import my_package.calculation as calc | calc | calc.add(1, 2) |
La forme la plus courante est from package import nom — le nom de fonction est court et le code côté main se lit proprement. Mais quand deux packages différents exportent le même nom de fonction et que tu as besoin des deux, la forme import package garde le nom de module visible pour qu'il soit évident de savoir lequel tu appelles.
- add — le nom lui-même atterrit dans la portée de main
- Appel :
add(1, 2)fonctionne directement - Court à écrire, mais ce n'est pas évident de quel package vient
add
- my_pkg — seul le nom du package atterrit dans la portée de main
- Appel :
my_pkg.calc.add(1, 2)avec le chemin pointé complet - Plus long à écrire, mais
my_pkg.calc.addrend l'origine évidente
from package import nom fait entrer le nom lui-même dans la portée de main (= appels courts). import package ne fait entrer que le nom du package, donc les appels utilisent le chemin pointé complet (= l'origine est explicite).Imports absolus vs relatifs
Et si des modules à l'intérieur d'un package devaient se référer entre eux ? Disons que utility/validator.py veut importer depuis utility/helper.py — il y a deux façons de l'écrire : import absolu et import relatif. Pour choisir entre les deux, tu dois d'abord être au clair sur ce que signifie la racine du projet.
- main.py ← le point d'entrée que tu lances en premier
- config.py ← réglages à l'échelle de l'app
- __init__.py ← s'exécute quand
import my_appse produit
- __init__.py
- validator.py
- helper.py
main.py (le point d'entrée). Le my_app dans l'import absolu from my_app.utility import validator se réfère au dossier de même nom directement sous cette racine.La racine du projet est le dossier qui contient le point d'entrée que tu lances avec python main.py. Quand tu écris l'import absolu from my_app.utility import validator, Python cherche my_app directement sous cette racine, puis descend dans utility/validator.py. Le chemin pointé reflète la structure de dossiers — c'est tout ce qu'est un import absolu.
| Type | Forme | Ce que ça signifie |
|---|---|---|
| Import absolu | from my_app.utility import helper | Chemin complet depuis la racine du projet |
| Import relatif | from . import helper | Relatif au fichier dans lequel tu te trouves |
| Import relatif (parent) | from .. import config | Cible un fichier un dossier plus haut |
. dans from . import signifie « le même dossier dans lequel je suis ».La répartition habituelle : le point d'entrée (main.py) utilise des imports absolus pour atteindre les packages, et les modules à l'intérieur d'un package utilisent des imports relatifs pour se référer entre eux. Avec les imports relatifs, renommer un dossier de package plus tard ne force pas de changements dans le code interne.
# my_app/utility/validator.py
# Import relatif pour helper.py dans le même package
from .helper import log_message
def validate_user(user):
if user.name and user.email:
log_message("OK")
return True
log_message("problème détecté")
return False
# my_app/utility/helper.py
def log_message(message):
print("[LOG]", message)
# Pour atteindre config.py dans un autre package,
# remonte d'un cran avec .. :
# from ..config import get_config
- config.py ← cible de
from ..config import ...
- validator.py ← écrit
from .helper import log_message - helper.py ← fournit
log_message(...)
from .helper import log_message dans validator.py utilise . — « ce dossier (= utility/) ». config.py est un niveau au-dessus dans my_app/, donc du point de vue de validator tu l'atteindrais comme ..config (.. = un niveau au-dessus).Les points d'entrée ne peuvent pas utiliser d'imports relatifs
Un fichier que tu lances directement avec python main.py ne peut pas utiliser d'imports relatifs — tu vas toucher un ImportError. Les imports relatifs ne fonctionnent que quand le fichier est chargé en tant que membre d'un package. Depuis le point d'entrée, utilise toujours des imports absolus (par ex. from my_app.utility import validate_user).
Une disposition style vrai projet
Avec ces règles, tu peux assembler des structures de dossiers qui correspondent à de vraies apps. Voici une disposition scindée en trois packages — settings, database et utility :
- main.py — point d'entrée (le fichier que tu lances avec python)
- config.py — réglages à l'échelle de l'app
- __init__.py — API publique du package my_app
- __init__.py
- connection.py / models.py
- __init__.py
- validator.py / helper.py
main.py récupère le package my_app/ entier ; les modules à l'intérieur utilisent des imports relatifs pour s'atteindre. Chaque sous-dossier a son propre __init__.py qui regroupe l'API publique de ce sous-package.Depuis main.py tu fais des appels vers l'extérieur avec des imports absolus comme from my_app.utility import validate_user. À l'intérieur de utility/, validator.py atteint helper.py avec from .helper import log_message — un import relatif. C'est la répartition canonique du travail.
À partir d'ici, c'est avancé — n'hésite pas à revenir en arrière si tu bloques
Le prochain exercice est le point d'orgue — construire deux packages en parallèle. Tu seras le plus à l'aise si le motif de ré-export via __init__.py te paraît naturel et que la distinction entre import absolu et relatif est solide pour toi. Si tu bloques, reviens à l'exercice précédent sur validator.py ou au schéma « Une disposition style vrai projet » ci-dessus pour te remettre en selle avant d'attaquer celui-ci.
Un challenge multi-dossiers
Il est temps d'essayer de construire deux packages en parallèle avec tout ce que tu as appris. Tu vas gérer un package produit catalog/ et un package billing/ dans des dossiers séparés, puis les orchestrer depuis main.py pour finaliser une commande. Répartir les responsabilités entre dossiers signifie que les changements aux données produit ne touchent que catalog/, et les changements au formatage de facture ne touchent que billing/ — la zone d'impact reste à l'intérieur d'un seul dossier.
- main.py — point d'entrée (utilise les deux packages pour traiter une commande)
- __init__.py — ré-exporte
from .products import get_price - products.py — implémente
get_price(name)
- __init__.py — ré-exporte
from .invoice import format_invoice - invoice.py — implémente
format_invoice(name, qty, unit_price)
catalog/ contient les données produit (products.py) ; billing/ contient le formatage de facture (invoice.py). Chaque __init__.py ré-exporte l'API publique, donc main.py peut faire des appels vers l'extérieur avec deux imports absolus : from catalog import get_price et from billing import format_invoice.__init__.py pour ré-exporter .products / .invoice.À partir d'ici, c'est du bonus — rare dans le code réel
La section ci-dessous couvre __all__ et est purement informative. Le code du monde réel n'utilise presque jamais from package import *, donc cette partie n'affecte pas le fil principal. Tant que tu maîtrises la forme import avec noms explicites, n'hésite pas à survoler cette section.
Contrôler from package import * avec __all__
Quand quelqu'un écrit from my_package import * pour tout récupérer avec une étoile, tu peux contrôler ce qui est exposé via __all__ dans __init__.py. Avec __all__ = ["add"], l'import * ne récupère que add — rien d'autre.
__all__ = ["add"] dans __init__.py, seul add passe via from package import *. multiply est laissé de côté (tu peux toujours l'attraper explicitement).# my_package/__init__.py
from .calculation import add, multiply
__all__ = ["add"] # seul add est exposé via *
# Côté appelant
# from my_package import *
# add(1, 2) # OK
# multiply(1, 2) # NameError (pas récupéré par *)
En pratique, évite les imports *
from my_package import * est dur à lire — tu ne peux pas dire en un coup d'œil ce qui est entré. Le code du monde réel utilise presque toujours des noms explicites comme from my_package import add, multiply. Considère __all__ comme un *filet de sécurité pour le cas rare où quelqu'un utilise ``**.
Cet article a couvert l'utilisation de __init__.py pour regrouper plusieurs modules en un seul package, la différence entre from package import nom et import package, le choix entre imports absolus et relatifs, et une disposition de dossiers proche d'un vrai projet.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Depuis my_app/utility/validator.py, quel est le bon import relatif pour charger helper.py dans le même dossier utility ?
Question 3Que se passe-t-il si tu écris un import relatif (from . import xxx) à l'intérieur d'un point d'entrée (un fichier que tu lances directement avec python main.py) ?