Dans cette partie, nous allons voir comment se structure une application Python en donnant un exemple d’arborescence. Celle-ci n’est pas figée mais est utilisée dans la plupart des projets en calcul scientifique (numpy, pandas, scikit-learn) et autres.
Une application Python est constituée
- de fichiers Python avec l’extension
.py
que l’on appelle des modules, - de répertoires contenant des fichiers Python que l’on appelle des packages.
Les modules et les packages peuvent être utilisés dans l’interpréteur Python en utilisant la commande import
. Lorsque l’on crée sa propre application, il est important de bien comprendre leur fonctionnement afin de définir les comportements que l’on souhaite pour l’utilisateur final lorsqu’il importe l’ensemble ou une partie du package.
Un module¶
Afin d’illustrer l’utilisation d’un module en Python, nous allons écrire un calculateur qui sait uniquement faire une addition et une soustraction.
Voici le fichier calculator_mod.py
.
%%file calculator_mod.py
"""
Calculator module
"""
def add(a, b):
"""
return a + b
"""
return a + b
def sub(a, b):
"""
return a - b
"""
return a - b
Utilisation d’un module¶
Il existe différentes manières d’importer un module en utilisant le mot-clef import
.
- On peut importer un module via son nom.
import calculator_mod
calculator_mod.add(1, 2)
- On peut importer une partie d’un module.
from calculator_mod import sub
sub(1, 2)
- On peut importer un module en modifiant son nom d’appel.
import calculator_mod as calc
calc.add(1, 2)
- On peut importer l’ensemble du module.
from calculator_mod import *
add(1, 2)
import
définit explicitement certains attributs du module
__dict__
: dictionnaire utilisé par le module pour l’espace de noms des attributs__name__
: nom du module__file__
: fichier du module__doc__
: documentation du module
print('file', calculator_mod.__file__)
print('name', calculator_mod.__name__)
print('doc', calculator_mod.__doc__)
Exécution d’un module¶
On peut ajouter à la fin d’un module le test suivant:
if __name__ == '__main__':
print(add(1, 2))
On peut à présent exécuter le module.
%%file calculator_mod.py
"""
Calculator module
"""
def add(a, b):
"""
return a + b
"""
return a + b
def sub(a, b):
"""
return a - b
"""
return a - b
if __name__ == '__main__':
print(add(1, 2))
! python calculator_mod.py
Un package¶
Comme dit en introduction, un package est un ensemble de modules Python. Prenons l’arborescence suivante
! tree examples/simple_calculator/
On trouve ici un package appelé calculator
et un sous-package appelé operator
dans lequel se trouvent deux modules (add.py
et sub.py
).
Le fichier __init__.py
¶
Prenons l’exemple du fichier calculator/operator/__init__.py
__all__ = ['add', 'sub']
De cette manière, on peut importer add
et sub
en faisant tout simplement
import sys
sys.path.append("./examples/simple_calculator/")
from calculator.operator import *
On accède ensuite aux attributs et aux fonctions en faisant
print(add.add(1, 2))
print(sub.sub(1, 2))
Il est également important d’utiliser l’import relatif lorsque l’on utilise des fonctionnalités de notre application dans les différents modules.
Prenons l’example du fichier calculator/__init__.py
from . import operator
from .operator import *
from .operator.add import add
On a alors le comportement suivant
import calculator
calculator.add(1, 2)
calculator.sub.sub(2,3)
Solution to Exercise 1
- non
- oui
- non
- oui
Recherche de modules et de packages¶
Pour que Python importe correctement un module, celui-ci doit être dans son PATH. Le module sys
permet de connaître la liste des répertoires où Python va rechercher les modules.
import sys
print(sys.path)
Python va donc rechercher dans
- le répertoire courant
- dans PYTHONPATH si défini (c’est la même syntaxe que le PATH)
- dans un répertoire par défaut
On peut également rajouter des répertoires à l’exécution étant donné que sys.path
n’est qu’une liste.
sys.path.append("/home/loic/Formations/")
print(sys.path)
Lorsque l’on veut importer foo, voici l’ordre des fichiers recherchés dans sys.path.
- foo.dll, foo.dylib ou foo.so
- foo.py
- foo.pyc
- foo/__init__.py
Réaliser sa première distribution¶
Le contenu à diffuser peut être de différents types
- des modules et des sous packages
- des données
- des scripts
- des dépendances
Il est nécessaire d’ajouter un ensemble de fichiers pour pouvoir faire le packaging.
Les indispensables
pyproject.toml
README.rst
ouREADME.md
LICENSE.txt
- votre
package
Les optionnels
MANIFEST.in
Voici une arborescence classique d’un package Python
package/
doc/
examples/
package/
...
tests/
tests/
LICENSE.txt
README.rst
pyproject.toml
Premier exemple de pyproject.toml
¶
Ce fichier est l’élément central de votre application. Il va non seulement décrire la façon de construire le package, de le définir ainsi que ses dépendances à l’installation mais il va également être utilisé pour la configuration de tous les outils qui vont vérifier la qualité de l’application (black, ruff, pytest, ...).
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "calculator"
dynamic = ["version"]
requires-python = ">=3.8"
[tool.setuptools]
packages = ["calculator"]
[tool.setuptools.dynamic]
version = { attr = "calculator.version.__version__" }
build-system
indique les outils pour construire le package ainsi que le backend utilisé. Nous utiliserons tout au long de cet ateliersetuptools
mais il en existe bien d’autres[1] (Hatch, PDM, Flit, Whey, ...).
project
définit les caractéristiques de votre package (son nom, sa version, ses dépendances, sa description, ses auteurs, ...).
Pour installer votre package, il suffit de faire
pip install .
pour installer le package dans le répertoiresite-packages
où se trouve votre version de Python.pip install . --user
pour l’installer dans votre$HOME
.pip install -e .
pour installer votre application en mode développement. Chaque fois que vous faites une modification, le package est automatiquement mis à jour (ce n’est en fait qu’un lien symbolique vers votre package).
Gérer les dépendances¶
Les dépendances sont gérées à travers le fichier pyproject.toml
. pixi
est en mesure de mettre à jour ce fichier lorsque vous utilisez la ligne de commande.
Supposons par exemple que votre package a une dépendance à NumPy pour que celui-ci fonctionne. Il vous suffit de faire
pixi add numpy
et vous verrez dans le fichier pyproject.toml
[tool.pixi.dependencies]
numpy = ">=2.0.0,<2.1"
Vous pouvez également l’ajouter à la main de la manière suivante
[project]
...
dependencies = [
"numpy",
]
De la même manière que ce qu’on a vu pour la définition des environnements de développement, il est possible de spécifier les versions que l’on souhaite.
[project]
...
dependencies = [
"numpy>=1.0,<1.13",
]
Il est également possible d’ajouter des dépendances optionnelles comme par exemple
[project.optional-dependencies]
test = ["pytest"]
doc = ["sphinx", "nbsphinx"]
On peut ensuite sélectionner ces options lors de l’installation
pip install .[test]
pip install .[test,doc]
Mais on préférera passer par pixi
en faisant l’équivalent
pixi add -f test pytest
pixi add -f doc sphinx nbsphinx
ce qui ajoutera les sections suivantes dans le fichier pyproject.toml
[tool.pixi.feature.test.dependencies]
pytest = ">=7.2.0,<7.3"
[tool.pixi.feature.doc.dependencies]
sphinx = ">=7.3.7,<7.4"
nbsphinx = ">=0.9.4,<0.10"
Il est ensuite possible de créer des environnements spécifiques à partir des features à l’aide de la commmande
pixi project environment add test -f test
pixi project environment add doc -f doc
Ce qui aura comme conséquence d’ajouter les lignes suivantes dans le fichier pyproject.toml
[tool.pixi.environments]
test = ["test"]
doc = ["doc"]
Pour utiliser l’environnement, il suffit de le spécifier via l’option -e
comme par exemple
pixi run -e test pytest
Nous reverrons les environnements dans les prochains chapitres lorsque nous parlerons de la documentation et des tests.
Ajout de scripts¶
En plus des sous packages et modules Python, votre application peut avoir des scripts que l’on peut vouloir exécuter en ligne de commande. Ce sont là encore des fichier Python. Ils sont spécifiés dans le fichier pyproject.toml
.
[project.scripts]
calculator-script = "calculator.command_line:main"
où calculator-script
est le nom de l’exécutable.
Ajouter une extension¶
Un package ne contient pas forcément uniquement des fichiers Python. On peut par exemple avoir des extensions écrites en cython
. Ce n’est pas si simple de les prendre en compte avec le fichier pyproject.toml
. Si vous souhaitez faire quelque chose de propre, il faut regarder du coté de NumPy ou de pandas. Ils utilise tous les deux le backend meson
pour construire leurs extensions Cython.
Afin de ne pas complexifier l’atelier, nous utiliserons l’ancienne méthode qui se conjugue avec le fichier pyproject.toml
. Pour ce faire, nous allons créer le fichier setup.py
avec le contenu suivant
from setuptools import setup, find_packages
from setuptools.extension import Extension
from Cython.Build import cythonize
extension = [Extension(name = "calculator.cython_mod",
sources = ["calculator/cython_mod.pyx"])
]
setup(
...
ext_modules=cythonize(extension),
...
)
Il faut également ajouter cython
dans les dépendances du build-system
.
[build-system]
requires = ["setuptools", "cython"]
build-backend = "setuptools.build_meta"
! tree TPs/1.packaging/step0/