Bonne pratique de Python

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

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

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.

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?

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.

Ecrire 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.

⚠️ 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.

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)

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é

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)

Parler de codecov

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.


  1. ↩︎

  2. 1: ↩︎

  3. ↩︎

  4. 2: ↩︎

Previous
Next