Les paquets

Les paquets (ou packages) forment une entité hiérarchique au-dessus des modules : un paquet est un module qui contient d’autre modules, un peu comme un dossier contient des fichiers.
D’ailleurs, les paquets prennent généralement la forme de dossiers sur le système de fichiers.

On en a déjà rencontré pendant ce cours : souvenez-vous du module xml.etree.ElementTree : il s’agissait en fait d’un module ElementTree dans un paquet xml.etree. On comprend par la même occasion qu'etree est lui-même imbriqué dans un paquet xml, car les paquets sont hiérarchiques.

Construction d'un paquet

Pour créer notre propre paquet, on peut alors simplement créer un nouveau répertoire dans lequel on placera nos fichiers Python (nos modules).

Créons par exemple un dossier operations depuis le répertoire courant, avec deux fichiers addition.py et soustraction.py :

def addition(a, b):
    return a + b
operations/addition.py
def soustraction(a, b):
    return a - b
operations/soustraction.py

Nous voilà maintenant avec un nouveau paquet operations. Ce paquet forme un espace de noms supplémentaire autour de nos modules, et nous devons donc les préfixer de operations. pour les importer.

>>> from operations import addition
>>> # on a importé le module addition
>>> addition.addition(1, 2)
3
>>> from operations.soustraction import soustraction
>>> # on a importé directement la fonction soustraction
>>> soustraction(1, 2)
-1

Pensez à bien vous placer depuis le répertoire contenant le dossier operations et non dans le dossier operations lui-même pour exécuter ce code.
Par exemple si votre dossier operations se trouve dans un dossier projet, il faut que vous exécutiez l’interpréteur depuis ce dossier projet sans quoi Python ne serait pas en mesure de trouver le paquet.

On remarque que l’on ne peut pas simplement faire import operations puis utiliser par exemple operations.addition.addition(1, 2) comme on le ferait avec des modules. C’est parce que les modules d’un paquet ne sont pas directement chargés quand le paquet est importé, mais nous verrons ensuite comment y parvenir.

Imports relatifs

Il peut arriver depuis un paquet que nous ayons besoin d’accéder à d’autres modules du même paquet. Par exemple, notre fonction soustraction pourrait vouloir faire appel à la fonction addition.
Pour cela, on va pouvoir importer la fonction addition dans le module soustraction, comme on vient de le faire dans l’interpréteur interactif.

from operations.addition import addition

def soustraction(a, b):
    return addition(a, -b)
operations/soustraction.py

Et tout fonctionne comme prévu :

>>> from operations.soustraction import soustraction
>>> soustraction(8, 5)
3

Mais la syntaxe peut paraître lourde, pourquoi avoir besoin de préciser operations alors qu’on est déjà dans ce paquet ? Python a prévu une réponse à ça : les imports relatifs.

Ainsi, pour un import au sein d’un même paquet, on peut simplement référencer un autre module en le préfixant d’un . sans indiquer explicitement le paquet (qui sera donc le paquet courant).

from .addition import addition

def soustraction(a, b):
    return addition(a, -b)
operations/soustraction.py

Et l’on peut vérifier en important le module operations.soustraction que tout fonctionne toujours correctement.

Attention cependant, cette syntaxe d’imports relatifs n’est valable que dans le cas d’un from ... import .... Il n’est ainsi pas possible d’écrire simplement import .addition pour importer le module addition.
En revanche la syntaxe from . import addition est valide (équivalente à from operations import addition).

Fichier __init__.py

Comme je le disais précédemment, le code des modules n’est pas directement chargé quand on importe le paquet. Qu’est-ce qui se passe alors quand on fait un import operations ?

À notre niveau pas grand chose en fait. Python identifie où se trouvent les fichiers du paquet operations et instancie un module vide qu’il nous renvoie.

Mais dans les faits, il cherche un fichier __init__.py à l’intérieur du paquet pour l’exécuter. C’est en fait ce fichier qui contient le code du paquet à proprement parler : tout ce qui sera présent dedans sera exécuté lors d’un import operations.

print('Hello')
operations/__init__.py
>>> import operations
Hello

Attention au nommage du fichier, il faut bien deux underscores de part et d’autre de init.

Bien sûr cet exemple n’est pas très utile, mais ce fichier __init__.py peut aussi nous servir à charger directement le code des modules du paquet.
Par exemple on peut y importer nos fonctions addition et soustraction pour les rendre accessibles plus facilement.

from .addition import addition
from .soustraction import soustraction
operations/__init__.py
>>> import operations
>>> operations.addition(3, 5)
8
>>> from operations import soustraction
>>> soustraction(8, 5)
3

Avant Python 3.3, le fichier __init__.py était nécessaire pour que Python considère le répertoire comme un paquet. Ce n’est plus le cas aujourd’hui mais ce fichier reste toutefois utile pour indiquer à Python que tout le code du paquet se trouve dans ce même répertoire.

Prenez ainsi l’habitude de toujours avoir un fichier __init__.py (même vide) dans vos paquets, cela pourrait vous éviter certaines déconvenues.

Fichier __main__.py

Il existe un autre fichier « magique » au sein des paquets, le fichier __main__.py Mais avant d’y revenir, je dois vous parler de l’option -m de l’interpréteur Python.

C’est une option qui permet de demander à Python d’exécuter un module à partir de son nom. Cela permet de ne pas avoir à connaître le chemin complet vers le fichier du module pour le lancer. Et certains modules Python s’en servent pour mettre à disposition des petits programmes.

Par exemple le module turtle propose une démo si on l’exécute via python -m turtle :

Démonstration de turtle.
python -m turtle

Cela fonctionne aussi avec nos propres modules.

def hello():
    print('Hello World!')

if __name__ == '__main__':
    hello()
hello.py
% python -m hello
Hello World!

Pour rappel, le bloc conditionnel if __name__ == '__main__' permet de placer du code qui sera exécuté uniquement quand le module est lancé directement (python hello.py ou python -m hello) mais pas quand le module est importé.

Dans le cadre de notre paquet, python -m operations cherchera à exécuter son fichier __main__.py. Nous pouvons alors y créer un tel fichier pour nous aussi faire une démonstration de notre paquet.

from .addition import addition
from .soustraction import soustraction

if __name__ == '__main__':
    print('Addition: 3+5 =', addition(3, 5))
    print('Soustraction: 8-3 =', soustraction(8, 3))
operations/__main__.py
% python -m operations
Addition: 3+5 = 8
Soustraction: 8-3 = 5

En conclusion, je vous invite à consulter mon billet Notes sur les modules et packages en Python qui répond à plusieurs problématiques au sujet des paquets et des imports en Python.