La notion de package permet de créer des bibliothèques plus structurées qu'avec un simple module.
Comme introduit précédemment, un module est donc un objet python qui correspond à la fois à:
Lorsqu'il s'agit d'implémenter une très grosse bibliothèque, il n'est pas concevable de tout concentrer en un seul fichier. C'est là qu'intervient la notion de package, qui est un peu aux répertoires ce que que le module est aux fichiers.
Pour illustrer par la pratique nous allons créer un package qui contient un module. Pour cela on crée une arborescence d&ans le répertoire courant:
package_jouet/ ├── __init__.py └── module_jouet.py
Ci dessous le contenu des fichiers __init__.py
et module_jouet.py
print("chargement du package", __name__) spam = ['a', 'b', 'c'] # on peut forcer l'import de modules import package_jouet.module_jouet # et définir des raccourcis jouet = package_jouet.module_jouet.jouet
print("Chargement du module", __name__, "dans le package 'package_jouet'") jouet = 'une variable définie dans package_jouet.module_jouet'
On Lance l'interpréteur dans le répertoire courant et on importe le package:
# L'importation du paquetage produit la sortie suivante: >>> import package_jouet chargement du package package_jouet Chargement du module package_jouet.module_jouet dans le package 'package_jouet'
Comme on le voit, le package porte le même nom que le répertoire, c'est-à-dire que, de même que le module module_simple
correspond au fichier module_simple.py
, le package python package_jouet
correspond au répertoire package_jouet
.
Importer un package revient essentiellement à charger, lorsqu'il existe, le fichier __init__.py dans le répertoire correspondant (et sinon, on obtient un package vide).
On a coutume de faire la différence entre package et module, mais en termes d'implémentation les deux objets sont en fait de même nature, ce sont des objets modules:
>>> type(package_jouet) module >>> type(package_jouet.module_jouet) module
Ainsi, le package se présente aussi comme un espace de nom. L'espace de noms du package permet de référencer les packages ou modules qu'il contient, comme on l'a vu ci-dessus, le package référence le module au travers de son attribut module_jouet:
>>> print(package_jouet.module_jouet) <module 'package_jouet.module_jouet' from '/tmp/yoann/package_jouet/module_jouet.py'>
Vous remarquerez que le module module_jouet
a été chargé au même moment que package_jouet
. Ce comportement n'est pas implicite ou prédéfini. C'est nous qui avons explicitement choisi d'importer ce module dans le package (en le définissant dans le fichier __init__.py).
Cette technique correspond à un usage assez fréquent, où on veut exposer directement dans l'espace de nom du package des symboles qui sont en réalité définis dans un module.
Avec le code présent dans __init__.py, après avoir importé package_jouet, nous pouvons utiliser la variable jouet
>>> package_jouet.jouet 'une variable définie dans package_jouet.module_jouet'
Alors qu'en fait, sans la référence jouet crée dans le package, il faudrait écrire pour y accéder:
>>> package_jouet.module_jouet.jouet 'une variable définie dans package_jouet.module_jouet'
Mais cela impose alors à l'utilisateur d'avoir une connaissance sur l'organisation interne de la bibliothèque, ce qui est considéré comme une mauvaise pratique.
D'abord, cela donne facilement des noms à rallonge et du coup nuit à la lisibilité, ce n'est pas pratique. Mais surtout, que se passerait-il alors si le développeur du package voulait renommer des modules à l'intérieur de la bibliothèque? On ne veut pas que ce genre de décision ait un impact sur les utilisateurs.
De manière générale, __init__.py peut contenir n'importe quel code Python chargé d'initialiser le package. Notez que depuis Python-3.3, la présence de __init__.py n'est plus strictement nécessaire.
Lorsqu'il est présent, comme pour les modules usuels, __init__.py n'est chargé qu'une seule fois par l'interpréteur Python; s'il rencontre plus tard à nouveau le même import, il l'ignore silencieusement.
Les objets de type module possèdent des attributs spéciaux:
__name__ | Nom canonique du module. |
---|---|
__file__ | L'emplacement du fichier duquel a été chargé le module. |
__all__ | Définir les symboles concernés par un import * (déconseillé en production) |
La mécanique des imports telle qu'on l'a vue jusqu'ici est ce qui s'appelle un import absolu qui est le mécanisme par défaut (depuis python-2.5): le module importé est systématiquement cherché à partir de sys.path
.
Dans ce mode de fonctionnement, si on trouve dans le même répertoire deux fichiers foo.py
et bar.py
, et que dans le premier on écrit:
# tentative d'import d'un fichier présent dans le même répertoire import bar
Le plus souvent, alors qu'il existe ici même un fichier bar.py
, l'import ne réussit pas (sauf si le répertoire courant est dans sys.path
mais en général ce n'est pas le cas).
Ce mécanisme d'import absolu a l'avantage d'éviter qu'un module local, par exemple “random.py”, ne vienne cacher le module “random” de la bibliothèque standard. Mais comment peut-on faire alors pour charger le module “random.py” local? C'est à cela que sert l'import relatif.
# Pour importer un module entier en mode relatif from . import random as local_random_module # La syntaxe pour importer seulement un symbole from .random import alea
Il faut savoir également qu'on peut “remonter” dans l'arborescence de fichiers en utilisant plusieurs points '.' consécutifs.
# Importer un symbole du module local random présent dans # le repertoire parent: deux points pour remonter from ..random import alea as imported
Lorsque deux modules sont situés dans le même répertoire, il semble naturel que l'import entre eux se fasse par un import relatif, plutôt que de devoir répéter ad nauseam le nom de la bibliothèque dans tous les imports. De plus, l'import relatif présente l'avantage d'être insensible aux renommages divers à l'intérieur d'une bibliothèque.
l'import relatif ne fonctionne pas toujours comme on pourrait s'y attendre. Le point important à garder en tête est que lors d'un import relatif, c'est l'attribut __name__ qui sert à déterminer le point de départ.
Concrètement, lorsque dans main.py on écrit:
# usage de l'import relatif from . import random
L'interpréteur:
Aussi cet import est-il retranscrit en:
from package_relatif import random
Or, le point d'entrée du programme (c'est-à-dire le fichier qui est passé directement à l'interpréteur python) est considéré comme un module dont l'attribut __name__ vaut la chaîne “main”.
Du coup, par construction, il n'est quasiment pas possible d'utiliser les imports relatifs à partir du script de lancement.
Pour pallier à ce type d'inconvénients, il a été introduit ultérieurement (voir PEP 366 ci-dessous) la possibilité pour un module de définir (écrire) l'attribut package, pour contourner cette difficulté
On voit que tout ceci est rapidement assez scabreux. Cela explique sans doute l'usage relativement peu répandu des imports relatifs.
De manière générale, une bonne pratique consiste à:
S'agissant des tests:
du coup les tests sont toujours exécutés avec une phrase comme
python3 -m unittest tests.jeu_de_tests
et dans ce contexte-là, il est possible par exemple pour les tests de recourir à l'import relatif.