Bonne pratique de Python

Les normes communautaires du monde de l’open-source ont permis une harmonisation de la structure des projets Python et des scripts. Ce chapitre évoque quelques-unes de ces conventions. Pour aller plus loin, un cours en 3e année d’ENSAE est disponible sur un autre site

Tutoriel
Rappels
Author

Lino Galiana

Published

2024-07-10

Une référence utile à lire est le Hitchhiker’s Guide to Python

1 Structure d’un projet en Python

La structure basique d’un projet développé en Python est la suivante, qu’on peut retrouver dans ce dépôt:

README.md
LICENSE
setup.py
requirements.txt
monmodule / __init__.py
monmodule / core.py
monmodule / helpers.py
docs / conf.py
docs / index.rst
tests / context.py
tests / test_basic.py
tests / test_advanced.py

Quelques explications et parallèles avec les packages R1 :

  • Le code Python est stocké dans un module nommé monmodule. C’est le coeur du code dans le projet. Contrairement à R, il est possible d’avoir une arborescence avec plusieurs modules dans un seul package. Un bon exemple de package dont le fonctionnement adopte une arborescence à plusieurs niveaux est scikit
  • Le fichier setup.py sert à construire le package monmodule pour en faire un code utilisable. Il n’est pas obligatoire quand le projet n’a pas vocation à être sur PyPi mais il est assez facile à créer en suivant ce template. C’est l’équivalent du fichier Description dans un package R (exemple)
  • Le fichier requirements.txt permet de contrôler les dépendances du projet. Il s’agit des dépendances nécessaires pour faire tourner les fonctions (par exemple numpy), les tester et construire automatiquement la documentation (par exemple sphinx). Dans un package R, le fichier qui contrôle l’environnement est le NAMESPACE.
  • Le dossier docs stocke la documentation du package. Le mieux est de le générer à partir de sphinx et non de l’éditer manuellement. (cf. plus tard). Les éléments qui s’en rapprochent dans un package R sont les vignettes.
  • Les tests génériques des fonctions. Ce n’est pas obligatoire mais c’est recommandé : ça évite de découvrir deux jours avant un rendu de projet que la fonction ne produit pas le résultat espéré.
  • Le README.md permet de créer une présentation du package qui s’affiche automatiquement sur github/gitlab et le fichier LICENSE vise à protéger la propriété intellectuelle. Un certain nombre de licences standards existent et peuvent être utilisées comme template grâce au site https://choosealicense.com/

2 La structure nécessaire des projets nécessaire pour pouvoir construire un package R est plus contrainte. Les packages devtools, usethis et testthat ont grandement facilité l’élaboration d’un package R. A cet égard, il est recommandé de lire l’incontournable livre d’Hadley Wickham

2 Style de programmation et de documentation

The code is read much more often than it is written.

Guido Van Rossum [créateur de Python]

Python est un langage très lisible. Avec un peu d’effort sur le nom des objets, sur la gestion des dépendances et sur la structure du programme, on peut très bien comprendre un script sans avoir besoin de l’exécuter. La communauté Python a abouti à un certain nombre de normes, dites PEP (Python Enhancement Proposal), qui constituent un standard dans l’écosystème Python. Les deux normes les plus connues sont la norme PEP8 (code) et la norme PEP257 (documentation).

La plupart de ces recommandations ne sont pas propres à Python, on les retrouve aussi dans R (cf. ici). On retrouve de nombreux conseils dans cet ouvrage qu’il est recommandé de suivre. La suite se concentrera sur des éléments complémentaires.

2.1 Import des modules

Les éléments suivants concernent plutôt les scripts finaux, qui appellent de multiples fonctions, que des scripts qui définissent des fonctions.

Un module est un ensemble de fonctions stockées dans un fichier .py. Lorsqu’on écrit dans un script

import modu

Python commence par chercher le fichier modu.py dans le dossier de travail. Il n’est donc pas une bonne idée d’appeler un fichier du nom d’un module standard de python, par exemple math.py ou os.py. Si le fichier modu.py n’est pas trouvé dans le dossier de travail, Python va chercher dans le chemin et s’il ne le trouve pas retournera une erreur.

Une fois que modu.py est trouvé, il sera exécuté dans un environnement isolé (relié de manière cohérente aux dépendances renseignées) et le résultat rendu disponible à l’interpréteur Python pour un usage dans la session via le namespace (espace où Python associe les noms donnés aux objets).

En premier lieu, ne jamais utiliser la syntaxe suivante :

# A NE PAS UTILISER
from modu import *

x = sqrt(4)  # Is sqrt part of modu? A builtin? Defined above?

L’utilisation de la syntaxe import * créé une ambiguité sur les fonctions disponibles dans l’environnement. Le code est ainsi moins clair, moins compartimenté et ainsi moins robuste. La syntaxe à privilégier est la suivante :

import modu

x = modu.sqrt(4)  # Is sqrt part of modu? A builtin? Defined above?

3 Structuration du code

Il est commun de trouver sur internet des codes très longs, généralement dans un fichier __init__.py (méthode pour passer d’un module à un package, qui est un ensemble plus structuré de fonctions). Contrairement à la légende, avoir des scripts longs est peu désirable et est même mauvais ; cela rend le code difficilement à s’approprier et à faire évoluer. Mieux vaut avoir des scripts relativement courts (sans l’être à l’excès…) qui font éventuellement appels à des fonctions définies dans d’autres scripts.

Pour la même raison, la multiplication de conditions logiques ifelse ifelse est généralement très mauvais signe (on parle de code spaghetti) ; mieux vaut utiliser des méthodes génériques dans ce type de circonstances.

4 Écrire des fonctions

Les fonctions sont un objet central en Python. La fonction idéale est une fonction qui agit de manière compartimentée : elle prend un certain nombre d’inputs et est reliée au monde extérieur uniquement par les dépendances, elle effectue des opérations sans interaction avec le monde extérieur et retourne un résultat. Cette définition assez consensuelle masque un certain nombre d’enjeux :

  • Une bonne gestion des dépendances nécessite d’avoir appliqué les recommandations évoquées précédemment
  • Isoler du monde extérieur nécessite de ne pas faire appel à un objet extérieur à l’environnement de la fonction. Autrement dit, aucun objet hors de la portée (scope) de la fonction ne doit être altéré ou utilisé.

Par exemple, le script suivant est mauvais au sens où il utilise un objet y hors du scope de la fonction add

def add(x):
    return x + y

Il faudrait revoir la fonction pour y ajouter un élément y:

def add(x, y):
    return x + y

Pycharm offre des outils de diagnostics très pratiques pour détecter et corriger ce type d’erreur.

4.1 ⚠️ aux arguments optionnels

La fonction la plus lisible (mais la plus contraignante) est celle qui utilise exclusivement des arguments positionnels avec des noms explicites.

Dans le cadre d’une utilisation avancée des fonctions (par exemple un gros modèle de microsimulation), il est difficile d’anticiper tous les objets qui seront nécessaires à l’utilisateur. Dans ce cas, on retrouve généralement dans la définition d’une fonction le mot-clé **kwargs (équivalent du ... en R) qui capture les arguments supplémentaires et les stocke sous forme de dictionnaire. Il s’agit d’une technique avancée de programmation qui est à utiliser avec parcimonie.

5 Documenter les fonctions

La documentation d’une fonction s’appelle le docstring. Elle prend la forme suivante :

def square_and_rooter(x):
    """Return the square root of self times self."""
    ...

Avec PyCharm, lorsqu’on utilise trois guillemets sous la définition d’une fonction, un template minimal à completer est automatiquement généré. Les normes à suivre pour que la docstrings soit reconnue par le package sphinx sont présentées dans la PEP257. Néanmoins, elles ont été enrichies par le style de docstrings NumPy qui est plus riche et permet ainsi des documentations plus explicites (voir ici et ici).

Suivre ces canons formels permet une lecture simplifiée du code source de la documentation. Mais cela a surtout l’avantage, lors de la génération d’un package, de permettre une mise en forme automatique des fichiers help d’une fonction à partir de la docstrings. L’outil canonique pour ce type de construction automatique est sphinx (dont l’équivalent R est Roxygen)

6 Les tests

Tester ses fonctions peut apparaître formaliste mais c’est, en fait, souvent d’un grand secours car cela permet de détecter et corriger des bugs précoces (ou au moins d’être conscient de leur existence). Au-delà de la correction de bug, cela permet de vérifier que la fonction produit bien un résultat espéré dans une expérience contrôlée.

En fait, il existe deux types de tests:

  • tests unitaires : on teste seulement une fonctionalité ou propriété
  • tests d’intégration : on teste l’intégration de la fonction dans un ensemble plus large de fonctionalités

Ici, on va plutôt se focaliser sur la notion de test unitaire, la notion de test d’intégration nécessitant d’avoir une chaîne plus complète de fonctions (mais il ne faut pas la négliger).

On peut partir du principe suivant :

toute fonctionnalité non testée comporte un bug

Le fichier tests/context.py sert à définir le contexte dans lequel le test de la fonction s’exécute, de manière isolée. On peut adopter le modèle suivant, en changeant import monmodule par le nom de module adéquat

import os
import sys

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

import monmodule

Chaque fichier du dossier de test (par exemple test_basic.py et test_advanced.py) incorpore ensuite la ligne suivante, en début de script

from .context import sample

Pour automatiser les tests, on peut utiliser le package unittest (doc ici). L’idée est que dans un cadre contrôlé (on connaît l’input et en tant que concepteur de la fonction on connaît l’output ou, a minima les propriétés de l’output) on peut tester la sortie d’une fonction.

La structure canonique de test est la suivante3

import unittest


def fun(x):
    return x + 1


class MyTest(unittest.TestCase):
    def test(self):
        self.assertEqual(fun(3), 4)

4 Le code équivalent avec R serait testthat::expect_equal(fun(3),4)

7 Partager

Ce point est ici évoqué en dernier mais, en fait, il est essentiel et mérite d’être une réflexion prioritaire. Tout travail n’a pas vocation à être public ou à dépasser le cadre d’une équipe. Cependant, les mêmes exigences qui s’appliquent lorsqu’un code est public méritent de s’appliquer avec un projet personnel. Avant de partager un code avec d’autres, on le partage avec le “futur moi”. Reprendre un code écrit il y a plusieurs semaines est coûteux et mérite d’anticiper en adoptant des bonnes pratiques qui rendront quasi-indolore la ré-appropriation du code.

L’intégration d’un projet avec git fiabilise grandement le processus d’écriture du code mais aussi, grâce aux outils d’intégration continue, la production de contenu (par exemple des visualisations html ou des rapports finaux écrits avec markdown). Il est recommandé d’immédiatement connecter un projet à git, même avec un dépôt qui aura vocation à être personnel. Les instructions d’utilisation de git sont détaillées ici.

Informations additionnelles

environment files have been tested on.

Latest built version: 2024-07-10

Python version used:

'3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:40:35) [GCC 12.3.0]'
Package Version
affine 2.4.0
aiobotocore 2.12.2
aiohttp 3.9.3
aioitertools 0.11.0
aiosignal 1.3.1
alembic 1.13.1
aniso8601 9.0.1
annotated-types 0.7.0
appdirs 1.4.4
archspec 0.2.3
astroid 3.1.0
asttokens 2.4.1
attrs 23.2.0
Babel 2.15.0
bcrypt 4.1.2
beautifulsoup4 4.12.3
black 24.4.2
blinker 1.7.0
blis 0.7.11
bokeh 3.4.0
boltons 23.1.1
boto3 1.34.51
botocore 1.34.51
branca 0.7.1
Brotli 1.1.0
cachetools 5.3.3
cartiflette 0.0.2
Cartopy 0.23.0
catalogue 2.0.10
cattrs 23.2.3
certifi 2024.2.2
cffi 1.16.0
charset-normalizer 3.3.2
click 8.1.7
click-plugins 1.1.1
cligj 0.7.2
cloudpathlib 0.18.1
cloudpickle 3.0.0
colorama 0.4.6
comm 0.2.2
commonmark 0.9.1
conda 24.3.0
conda-libmamba-solver 24.1.0
conda-package-handling 2.2.0
conda_package_streaming 0.9.0
confection 0.1.5
contextily 1.6.0
contourpy 1.2.1
cryptography 42.0.5
cycler 0.12.1
cymem 2.0.8
cytoolz 0.12.3
dask 2024.4.1
dask-expr 1.0.10
debugpy 1.8.1
decorator 5.1.1
dill 0.3.8
distributed 2024.4.1
distro 1.9.0
docker 7.0.0
duckdb 0.10.1
en-core-web-sm 3.7.1
entrypoints 0.4
et-xmlfile 1.1.0
exceptiongroup 1.2.0
executing 2.0.1
fastjsonschema 2.19.1
fiona 1.9.6
flake8 7.0.0
Flask 3.0.2
folium 0.16.0
fontawesomefree 6.5.1
fonttools 4.51.0
frozenlist 1.4.1
fsspec 2023.12.2
GDAL 3.8.4
gensim 4.3.2
geographiclib 2.0
geopandas 0.12.2
geoplot 0.5.1
geopy 2.4.1
gitdb 4.0.11
GitPython 3.1.43
google-auth 2.29.0
graphene 3.3
graphql-core 3.2.3
graphql-relay 3.2.0
graphviz 0.20.3
great-tables 0.10.0
greenlet 3.0.3
gunicorn 21.2.0
htmltools 0.5.2
hvac 2.1.0
idna 3.6
imageio 2.34.2
importlib_metadata 7.1.0
importlib_resources 6.4.0
inflate64 1.0.0
ipykernel 6.29.3
ipython 8.22.2
ipywidgets 8.1.2
isort 5.13.2
itsdangerous 2.1.2
jedi 0.19.1
Jinja2 3.1.3
jmespath 1.0.1
joblib 1.3.2
jsonpatch 1.33
jsonpointer 2.4
jsonschema 4.21.1
jsonschema-specifications 2023.12.1
jupyter-cache 1.0.0
jupyter_client 8.6.1
jupyter_core 5.7.2
jupyterlab_widgets 3.0.10
kaleido 0.2.1
kiwisolver 1.4.5
kubernetes 29.0.0
langcodes 3.4.0
language_data 1.2.0
lazy_loader 0.4
libmambapy 1.5.7
llvmlite 0.42.0
locket 1.0.0
lxml 5.2.2
lz4 4.3.3
Mako 1.3.2
mamba 1.5.7
mapclassify 2.6.1
marisa-trie 1.2.0
Markdown 3.6
markdown-it-py 3.0.0
MarkupSafe 2.1.5
matplotlib 3.8.3
matplotlib-inline 0.1.6
mccabe 0.7.0
mdurl 0.1.2
menuinst 2.0.2
mercantile 1.2.1
mizani 0.11.4
mlflow 2.11.3
mlflow-skinny 2.11.3
msgpack 1.0.7
multidict 6.0.5
multivolumefile 0.2.3
munkres 1.1.4
murmurhash 1.0.10
mypy 1.9.0
mypy-extensions 1.0.0
nbclient 0.10.0
nbformat 5.10.4
nest_asyncio 1.6.0
networkx 3.3
nltk 3.8.1
numba 0.59.1
numpy 1.26.4
oauthlib 3.2.2
opencv-python-headless 4.9.0.80
openpyxl 3.1.5
OWSLib 0.28.1
packaging 23.2
pandas 2.2.1
paramiko 3.4.0
parso 0.8.4
partd 1.4.1
pathspec 0.12.1
patsy 0.5.6
Pebble 5.0.7
pexpect 4.9.0
pickleshare 0.7.5
pillow 10.3.0
pip 24.0
pkgutil_resolve_name 1.3.10
platformdirs 4.2.0
plotly 5.19.0
plotnine 0.13.6
pluggy 1.4.0
polars 0.20.31
preshed 3.0.9
prometheus_client 0.20.0
prometheus-flask-exporter 0.23.0
prompt-toolkit 3.0.42
protobuf 4.25.3
psutil 5.9.8
ptyprocess 0.7.0
pure-eval 0.2.2
py7zr 0.20.8
pyarrow 15.0.0
pyarrow-hotfix 0.6
pyasn1 0.5.1
pyasn1-modules 0.3.0
pybcj 1.0.2
pycodestyle 2.11.1
pycosat 0.6.6
pycparser 2.21
pycryptodomex 3.20.0
pydantic 2.8.2
pydantic_core 2.20.1
pyflakes 3.2.0
Pygments 2.17.2
PyJWT 2.8.0
pylint 3.1.0
PyNaCl 1.5.0
pynsee 0.1.7
pyOpenSSL 24.0.0
pyparsing 3.1.2
pyppmd 1.1.0
pyproj 3.6.1
pyshp 2.3.1
PySocks 1.7.1
python-dateutil 2.9.0
python-dotenv 1.0.1
python-magic 0.4.27
pytz 2024.1
pyu2f 0.1.5
pywaffle 1.1.1
PyYAML 6.0.1
pyzmq 25.1.2
pyzstd 0.16.0
QtPy 2.4.1
querystring-parser 1.2.4
rasterio 1.3.10
referencing 0.34.0
regex 2023.12.25
requests 2.31.0
requests-cache 1.2.1
requests-oauthlib 2.0.0
rich 13.7.1
rpds-py 0.18.0
rsa 4.9
Rtree 1.2.0
ruamel.yaml 0.18.6
ruamel.yaml.clib 0.2.8
s3fs 2023.12.2
s3transfer 0.10.1
scikit-image 0.24.0
scikit-learn 1.4.1.post1
scipy 1.13.0
seaborn 0.13.2
setuptools 69.2.0
shapely 2.0.3
shellingham 1.5.4
six 1.16.0
smart_open 7.0.4
smmap 5.0.0
snuggs 1.4.7
sortedcontainers 2.4.0
soupsieve 2.5
spacy 3.7.5
spacy-legacy 3.0.12
spacy-loggers 1.0.5
SQLAlchemy 2.0.29
sqlparse 0.4.4
srsly 2.4.8
stack-data 0.6.2
statsmodels 0.14.1
tabulate 0.9.0
tblib 3.0.0
tenacity 8.2.3
texttable 1.7.0
thinc 8.2.5
threadpoolctl 3.4.0
tifffile 2024.7.2
tomli 2.0.1
tomlkit 0.12.4
toolz 0.12.1
topojson 1.9
tornado 6.4
tqdm 4.66.2
traitlets 5.14.2
truststore 0.8.0
typer 0.12.3
typing_extensions 4.11.0
tzdata 2024.1
Unidecode 1.3.8
url-normalize 1.4.3
urllib3 1.26.18
wasabi 1.1.3
wcwidth 0.2.13
weasel 0.4.1
webdriver-manager 4.0.1
websocket-client 1.7.0
Werkzeug 3.0.2
wheel 0.43.0
widgetsnbextension 4.0.10
wordcloud 1.9.3
wrapt 1.16.0
xgboost 2.0.3
xlrd 2.0.1
xyzservices 2024.4.0
yarl 1.9.4
yellowbrick 1.5
zict 3.0.0
zipp 3.17.0
zstandard 0.22.0

View file history

SHA Date Author Description
c9f9f8a 2024-04-24 15:09:35 Lino Galiana Dark mode and CSS improvements (#494)
d75641d 2024-04-22 18:59:01 Lino Galiana Editorialisation des chapitres de manipulation de données (#491)
005d89b 2023-12-20 17:23:04 Lino Galiana Finalise l’affichage des statistiques Git (#478)
a06a268 2023-11-23 18:23:28 Antoine Palazzolo 2ème relectures chapitres ML (#457)
09654c7 2023-11-14 15:16:44 Antoine Palazzolo Suggestions Git & Visualisation (#449)
889a71b 2023-11-10 11:40:51 Antoine Palazzolo Modification TP 3 (#443)
7221e7b 2023-10-10 14:00:44 Thomas Faria Relecture Thomas TD Pandas (#431)
a771183 2023-10-09 11:27:45 Antoine Palazzolo Relecture TD2 par Antoine (#418)
154f09e 2023-09-26 14:59:11 Antoine Palazzolo Des typos corrigées par Antoine (#411)
9a4e226 2023-08-28 17:11:52 Lino Galiana Action to check URL still exist (#399)
3bdf3b0 2023-08-25 11:23:02 Lino Galiana Simplification de la structure 🤓 (#393)
78ea2cb 2023-07-20 20:27:31 Lino Galiana Change titles levels (#381)
2dbf853 2023-07-05 11:21:40 Lino Galiana Add nice featured images (#368)
d201e3c 2022-08-03 15:50:34 Lino Galiana Pimp la homepage ✨ (#249)
12965ba 2022-05-25 15:53:27 Lino Galiana :launch: Bascule vers quarto (#226)
2f7b52d 2021-07-20 17:37:03 Lino Galiana Improve notebooks automatic creation (#120)
aeb3995 2021-07-06 11:11:03 avouacr Relecture et ajouts sur anaconda + jupyter (#116)
4cdb759 2021-05-12 10:37:23 Lino Galiana :sparkles: :star2: Nouveau thème hugo :snake: :fire: (#105)
f9f00cc 2020-09-15 21:05:54 Lino Galiana enlève quelques TO DO
c3c7433 2020-09-15 12:41:26 Lino Galiana Improve CI with gitlab and jupyter nb conversion (#35)
913047d 2020-09-08 14:44:41 Lino Galiana Harmonisation des niveaux de titre (#17)
56f8532 2020-09-08 10:40:03 Lino Galiana Reprise des éléments de la première séance dans le site web (#14)
725d4e1 2020-07-24 13:00:45 Lino Galiana Merge branch ‘master’ into pandas_intro
66176dc 2020-07-23 17:00:57 Lino Galiana fix typo in footnotes
0b4d3d1 2020-07-23 11:53:00 Lino Galiana Ecnore un mot
aa9d09f 2020-07-23 11:48:18 Lino Galiana Référence au hitch guide
a2facfb 2020-07-23 11:46:04 Lino Galiana Un peu de détails
d2c7518 2020-07-23 11:45:48 Lino Galiana Ajout sur les tests et gitignore
304985b 2020-07-23 10:47:30 Lino Galiana Mot sur sphinx
e8395c3 2020-07-23 10:12:09 Lino Galiana On verra pour les fonctions imbriquées
dcff627 2020-07-23 10:08:46 Lino Galiana change title level
98b6102 2020-07-23 10:02:17 Lino Galiana Elements supplémentaires sur la lisibilité
c376c32 2020-07-23 09:45:51 Lino Galiana Plus sur la structure
df95626 2020-07-22 18:36:06 Lino Galiana Met le lien quelque part
179b4bc 2020-07-22 18:33:04 Lino Galiana Topo sur la lisibilité du code
0556057 2020-07-22 18:02:29 Lino Galiana tests après
6ddd071 2020-07-22 17:52:30 Lino Galiana Partager
97419d0 2020-07-22 17:42:09 Lino Galiana quelques mots sur les dépendances
2849bac 2020-07-22 17:23:35 Lino Galiana quelques mots sur les dépendances
77f71f6 2020-07-22 16:20:12 Lino Galiana ptit mot sur les tests
f17fc8d 2020-07-22 14:36:48 Lino Galiana Griffoner des choses
Back to top

Footnotes

  1. 1↩︎

  2. 1:↩︎

  3. 2↩︎

  4. 2:↩︎

Citation

BibTeX citation:
@book{galiana2023,
  author = {Galiana, Lino},
  title = {Python Pour La Data Science},
  date = {2023},
  url = {https://pythonds.linogaliana.fr/},
  doi = {10.5281/zenodo.8229676},
  langid = {en}
}
For attribution, please cite this work as:
Galiana, Lino. 2023. Python Pour La Data Science. https://doi.org/10.5281/zenodo.8229676.