{{tag>cours python package}}
====== Python: notion de package ======
La notion de package permet de créer des bibliothèques plus structurées qu'avec un simple module.
Comme introduit [[570_importation_module_et_espaces_nommage|précédemment]], un module est donc un objet python qui correspond à la fois à:
* un (seul) fichier sur le disque;
* un espace de nom pour les variables du programme.
===== Package =====
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''.
Par le passé, pour définir un package, il fallait obligatoirement créer dans le répertoire (celui, donc, que l'on veut exposer à python), un fichier nommé %%__init__.py%%; ce n'est plus le cas depuis Python-3.3.
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)
===== Rôle du fichier __init.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.
===== Attributs spéciaux =====
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) |
===== Import absolu =====
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).
===== Import relatif =====
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.
==== Spécificités de l'import relatif ====
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:
* détermine que dans main.py, %%__name__%% vaut package_relatif.main;
* il "oublie" le dernier morceau main pour calculer que le package courant est package_relatif
* et c'est ce nom qui sert à déterminer le point de départ de l'import relatif.
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__".
==== Attribut __package__ ====
* le point d'entrée (celui qui est donné à python sur la ligne de commande) voit comme valeur pour __name__ la constante "__main__",
* et le mécanisme d'import relatif se base sur __name__ pour localiser les modules importés.
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é
==== En résumé ====
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 à:
* Considérer votre ou vos points d'entrée comme des accessoires; un point d'entrée se contente typiquement d'importer une classe d'un module, de créer une instance et de lui envoyer une méthode;
* toujours placer ces points d'entrée dans un répertoire séparé;
* notamment si vous utilisez setuptools pour distribuer votre application via pypi.org, vous verrez que ces points d'entrée sont complètement pris en charge par les outils d'installation.
S'agissant des tests:
* la technique qu'on a vue rapidement (de tester si __name__ vaut "__main__") est extrêmement basique et limitée. Le mieux est de ne pas l'utiliser en fait, en dehors de micro-maquettes.
* En pratique on écrit les tests dans un répertoire séparé (souvent appelé tests) et en tirant profit de la librairie unittest.
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.