Les packages, __init__.py et if __name__ == "__main__"

Objectifs de la leçon
  • Expliquer ce qu’est un package et en construire un avec __init__.py
  • Importer des modules et des fonctions depuis un package
  • Expliquer quelle est la valeur de __name__ quand un fichier est exécuté et quand il est importé
  • Utiliser if __name__ == "__main__": pour séparer une « bibliothèque » d’un « programme »

Qu’allons-nous apprendre ?

  • Le package — un dossier qui contient des modules.
  • Le fichier __init__.py et son rôle.
  • Importer depuis un package.
  • La variable spéciale __name__, et le modèle if __name__ == "__main__":.

Pourquoi en avons-nous besoin ?

Quand il y a 3 modules, ils peuvent être dans le même dossier. Quand il y en a 30, il faut des dossiers : tout ce qui concerne les utilisateurs dans un dossier, tout ce qui concerne les paiements dans un autre. C’est ainsi que sont construits tous les vrais projets, et aussi toutes les bibliothèques que vous installerez (FastAPI est un énorme package avec des dizaines de sous-packages).

Et if __name__ == "__main__": résout le problème de la leçon précédente : du code de test qui s’exécute chaque fois que quelqu’un importe le fichier.

Explication à partir des bases

Package = un dossier de modules

Un package est un dossier qui contient des modules (des fichiers .py), et généralement aussi un fichier nommé __init__.py.

store/                    ← project folder
├── main.py               ← the program you run
└── inventory/            ← a PACKAGE
    ├── __init__.py       ← marks this folder as a package
    ├── products.py       ← module
    └── stock.py          ← module

__init__.py

  • Marque le dossier comme un package, de façon explicite et claire.
  • S’exécute automatiquement la première fois que quelque chose est importé du package.
  • Généralement vide. On peut y mettre des imports pratiques, pour pouvoir écrire from inventory import add_product au lieu de from inventory.products import add_product.

(En Python moderne, on peut aussi importer depuis un dossier sans __init__.py, mais la convention en usage est de l’inclure, pour qu’il soit clair que c’est un package et pas un simple dossier.)

Importer depuis un package

Le point sépare un dossier d’un module, exactement comme / dans un chemin :

import inventory.products                 # inventory/products.py
from inventory import products            # same module, shorter use
from inventory.products import add_product   # one function

__name__ — « qui suis-je ? »

Chaque module a une variable intégrée nommée __name__. Python fixe sa valeur selon la façon dont le fichier a été lancé :

Comment le fichier a été lancéLa valeur de __name__
Exécuté directement : python prices.py"__main__"
Importé depuis un autre fichier : import prices"prices" (le nom du module)

Et donc :

if __name__ == "__main__":
    # runs ONLY when this file is run directly, NOT when it is imported
Analogie : un chef qui est aussi fournisseur

Un chef cuisine dans son propre restaurant (exécution directe), et en même temps vend des sauces aux magasins (import). Quand il est au restaurant, il sert aussi les clients : c’est le code dans if __name__ == "__main__". Quand un magasin achète une sauce, il reçoit seulement la sauce (les fonctions), et pas un repas entier qu’on lui imposerait.

Syntaxe

project/
├── main.py
└── package_name/
    ├── __init__.py
    └── module_name.py
# main.py
from package_name.module_name import some_function
from package_name import module_name

# any module that can also be run directly:
def main():
    ...          # what happens when the file is run as a program

if __name__ == "__main__":
    main()

Le modèle d’une fonction main() + la condition à la fin du fichier est la norme dans tout fichier Python destiné à être exécuté.

Premier exemple de code

Voyons __name__ en action. week-2/name_demo/greetings.py :

def hello(name):
    return "Hello, " + name + "!"

print("greetings.py: __name__ is", __name__)

if __name__ == "__main__":
    print("Running greetings.py directly - quick self test:")
    print(hello("Tester"))

week-2/name_demo/app.py :

import greetings

print("app.py: __name__ is", __name__)
print(greetings.hello("Dana"))

Exécution 1 : python greetings.py

greetings.py: __name__ is __main__
Running greetings.py directly - quick self test:
Hello, Tester!

Exécution 2 : python app.py

greetings.py: __name__ is greetings
app.py: __name__ is __main__
Hello, Dana!

Analyse du code ligne par ligne

Exécution 1 (python greetings.py)Exécution 2 (python app.py)
__name__ de greetings.py"__main__" (c’est le fichier exécuté)"greetings" (il a été importé)
Le premier print de greetingsS’exécuteS’exécute (le code au niveau supérieur s’exécute toujours à l’import)
Le bloc if __name__ == "__main__"S’exécute (auto-test)Ne s’exécute pas
__name__ de app.py—"__main__"

La conclusion : tout ce qui doit s’exécuter uniquement quand le fichier est lancé directement (tests, démonstrations, démarrage du programme) va dans la condition. Les définitions de fonctions et de constantes restent à l’extérieur, pour pouvoir être importées.

Un autre exemple

Un projet avec un vrai package. La structure :

week-2/store/
├── main.py
└── inventory/
    ├── __init__.py
    ├── products.py
    └── stock.py

inventory/__init__.py :

from inventory.products import create_product
from inventory.stock import add_stock, remove_stock

inventory/products.py :

def create_product(name, price):
    if price < 0:
        raise ValueError("price cannot be negative, got " + str(price))
    return {"name": name, "price": price, "quantity": 0}

if __name__ == "__main__":
    print(create_product("Test item", 10))

inventory/stock.py :

def add_stock(product, amount):
    product["quantity"] = product["quantity"] + amount

def remove_stock(product, amount):
    if amount > product["quantity"]:
        raise ValueError("not enough " + product["name"] + " in stock")
    product["quantity"] = product["quantity"] - amount

main.py :

from inventory import create_product, add_stock, remove_stock

def main():
    laptop = create_product("Laptop", 3500)
    add_stock(laptop, 10)
    remove_stock(laptop, 3)
    print(laptop)

    try:
        remove_stock(laptop, 50)
    except ValueError as error:
        print("Error:", error)

if __name__ == "__main__":
    main()

Exécution (depuis week-2/store) : python main.py

{'name': 'Laptop', 'price': 3500, 'quantity': 7}
Error: not enough Laptop in stock

Analyse :

  • __init__.py rassemble les fonctions importantes du package. Grâce à lui, main.py écrit from inventory import create_product, et n’a pas besoin de savoir dans quel fichier interne elle se trouve exactement. De l’extérieur, le package ressemble à « une seule unité ».
  • add_stock(laptop, 10) modifie le dict lui-même, car un dict est Mutable (le thème structures de données et fonctions !). La fonction n’a rien besoin de renvoyer.
  • products.py contient un auto-test dans if __name__ == "__main__". On peut l’exécuter seul pour le tester, et il ne « fait pas de désordre » quand il est importé.
  • main.py est enveloppé dans main() + la condition. Si quelqu’un l’importe à l’avenir, rien ne s’exécutera tout seul.
Comment exécuter un fichier qui se trouve dans un package ?

Si vous essayez python inventory/products.py, vous obtenez parfois une ModuleNotFoundError, car Python cherche les modules depuis le dossier du fichier exécuté (inventory/), et non depuis le dossier du projet. La bonne façon d’exécuter un module dans un package est depuis le dossier du projet, avec l’option -m (module) et des points à la place des barres obliques : python -m inventory.products.

Erreur courante

Erreur 1 : écrire "main" au lieu de "__main__".

if __name__ == "main":
    main()

Rien ne s’exécute, et il n’y a aucune erreur ! La valeur est "__main__", avec deux tirets bas de chaque côté. Une Logical Error silencieuse.

Erreur 2 : un import circulaire (Circular Import).

a.py importe b, et b.py importe a :

ImportError: cannot import name 'x' from partially initialized module 'a' (most likely due to a circular import)

Deux modules qui dépendent l’un de l’autre sont le signe d’un mauvais découpage. Déplacez la partie commune dans un troisième module, dont les deux importent.

Erreur 3 : mettre toute la logique dans le bloc if.

if __name__ == "__main__":
    def calculate(x):
        return x * 2
    print(calculate(5))

Maintenant, calculate ne peut pas être importée depuis un autre fichier, car elle n’est définie que lorsque le fichier est lancé directement. Les fonctions vont à l’extérieur, et seul le démarrage du programme va à l’intérieur.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: La structure d’un package

Dessinez la structure de dossiers d’un projet store avec un fichier main.py et un package nommé inventory, qui contient deux modules : products.py et stock.py. Quel fichier doit se trouver dans inventory, et pourquoi ?

Afficher la solution
store/
├── main.py
└── inventory/
    ├── __init__.py
    ├── products.py
    └── stock.py

__init__.py marque le dossier comme un package — un dossier de modules depuis lequel on peut importer. Il peut être vide.

Exercice 2: Importer depuis un package

Dans inventory/products.py, il y a une fonction create_product(name, price) qui renvoie un dictionnaire. Écrivez dans main.py trois façons de l’importer et de l’utiliser.

Afficher la solution
import inventory.products
laptop = inventory.products.create_product("Laptop", 3500)

from inventory import products
mouse = products.create_product("Mouse", 80)

from inventory.products import create_product
desk = create_product("Desk", 900)

Le point dans le chemin (inventory.products) est comme / dans les dossiers : le module products dans le package inventory.

Exercice 3: Qu’est-ce que name ?

Dans le fichier greetings.py :

def hello(name):
    return "Hello, " + name + "!"

print("__name__ is", __name__)

Qu’est-ce qui sera affiché dans chaque cas ?

  1. Vous lancez python greetings.py.
  2. Vous lancez main.py, qui contient import greetings.
Afficher la solution
  1. En exécution directe : __name__ is __main__.
  2. À l’import : __name__ is greetings — le nom du module.

Python fixe __name__ selon la façon dont le fichier a été lancé. C’est la base du main guard.

Exercice 4: Le main guard

Ajoutez à greetings.py (de l’exercice précédent, avec la fonction hello) un auto-test qui ne s’exécute que lorsque le fichier est lancé directement, et pas quand il est importé. Le test affiche quick self test: puis le résultat de hello("Tester").

Supprimez la ligne print("__name__ is", __name__) de l’exercice précédent.

Résultat attendu (python greetings.py) :

quick self test:
Hello, Tester!
Afficher la solution
def hello(name):
    return "Hello, " + name + "!"


if __name__ == "__main__":
    print("quick self test:")
    print(hello("Tester"))
quick self test:
Hello, Tester!

Quand un autre fichier fait import greetings, la condition vaut False — le test ne s’exécute pas, et seule la fonction est disponible.

Exercice 5: Le package mathtools

Construisez un package nommé mathtools avec deux modules : basic.py avec add(a, b), et stats.py avec mean(numbers) et maximum(numbers). Dans main.py, utilisez les trois.

add(2, 3)
mean([10, 20, 30])
maximum([10, 20, 30])

Résultat attendu :

add: 5
mean: 20.0
maximum: 30
Afficher la solution
math_project/
├── main.py
└── mathtools/
    ├── __init__.py
    ├── basic.py
    └── stats.py

mathtools/basic.py

def add(a, b):
    return a + b

mathtools/stats.py

def mean(numbers):
    return sum(numbers) / len(numbers)


def maximum(numbers):
    return max(numbers)

main.py

from mathtools.basic import add
from mathtools import stats

print("add:", add(2, 3))
print("mean:", stats.mean([10, 20, 30]))
print("maximum:", stats.maximum([10, 20, 30]))

Résumé

  • Package = un dossier de modules, avec un __init__.py qui s’exécute au premier import et peut « exposer » des noms pratiques.
  • Importer depuis un package : from package.module import name. Le point = entrer dans un dossier.
  • __name__ vaut "__main__" quand le fichier a été lancé directement, et le nom du module quand il a été importé.
  • if __name__ == "__main__": main() — démarrer le programme uniquement en exécution directe. Définissez les fonctions à l’extérieur.
  • Un module dans un package s’exécute depuis le dossier du projet avec python -m package.module.

Documentation officielle