Structure d'un projet Python, P2C

Ismail Mebsout
23 octobre 2024
5 min
Sommaire

En tant que développeur, je trouve que coder est un art. Lorsqu'on travaille sur un projet complexe, il y a de nombreuses étapes à suivre pour développer un code cohérent, solide et durable, qui pourra être lu et repris par d'autres contributeurs :

Étapes E2E du projet
  • Tout d'abord, il est essentiel de comprendre le problème pour répondre au bon besoin
  • Le projet peut être découpé en sous-projets qui facilitent la tâche et surtout la collaboration
  • Votre pipeline est le résultat direct de vos sous-projets
  • La structure du code doit suivre la même fragmentation, afin que vous puissiez avoir le même Pipeline et la même logique technique
  • Des baby steps peuvent être suivis dans chaque fragment afin de développer votre code efficacement

Dans cet article, nous verrons comment passer d'un besoin métier à un code Python entièrement fonctionnel et simple à appréhender.

Le sommaire est le suivant :

  1. Exemple de projet
  2. Organisation du projet
  3. Imports en Python

Exemple de projet

À titre d'illustration, nous considérerons le besoin métier suivant :

En tant que gestionnaire d'autoroute, je souhaite effectuer un comptage quotidien des véhicules empruntant un itinéraire donné. Pour répondre à ce besoin, une équipe Data Science a été mise sur le projet et a décidé d'utiliser une caméra fixe et de compter le nombre unique de plaques d'immatriculation.
(une suggestion parmi d'autres)

Cette idée peut être vue comme la séquence de plusieurs étapes (une décomposition simplifiée parmi d'autres) :

  • Détection des véhicules
  • Détection des plaques d'immatriculation
  • OCRisation des plaques d'immatriculation

D'où le pipeline suivant :

Pipeline technique

Organisation du projet

Étant donné le pipeline ci-dessus, on peut organiser le code comme suit :

|--data/ #useful to store temporary files for instance
|--Tests/ #hosts the functional unit testings of your code/api
|--notebooks/ #helpful for testing and developping and debugging
|--|--develop.ipynb
|--weights/ #weights are kept a part for easier manipulation
|--counthighwaypy/ #folder hosting your entire code
|--|--detectvehicle/ #1st brick of your pipeline
|--|--|--detect_vehicle_main.py
|--|--|--detect_vehicle_utils.py
|--|--|--detect_vehicle_conf.py
|--|--|--Tests/ #independent unit testings relative to 1st brick
|--|--detectlicenseplate/ #2nd brick of your pipeline
|--|--|--licence_plate_main.py
|--|--|--licence_plate_utils.py
|--|--|--licence_plate_conf.py
|--|--|--Tests/ #independent unit testings relative to 1st brick
|--|--ocrlicenseplate/ #3rd brick of your pipeline
|--|--|--ocr_license_main.py
|--|--|--ocr_license_utils.py
|--|--|--ocr_license_conf.py
|--|--|--Tests/ #independent unit testings relative to 1st brick

|--|--utils.py
|--|--conf.py #! very  important file (see below)
|--|--main.py # orchestrator of the different bricks
|--|--Tests/ #E2E technical unit testings
+--README.md
+--app.py #hosts your API and calls the main.py
+--packages.txt #python environment
+--launch_tests.sh #functional unit testings
+--pytest.ini
+--Dockerfile

Comme mentionné dans la section précédente, la structure du repository suit la même logique que celle du pipeline.

  • Chaque brique contient :
    + un fichier utils : contient toutes les fonctions auxiliaires de votre brique
    + un fichier conf : contient tous les paramètres constants (noms de variables, dossiers, valeurs des hyperparamètres, …)
    + un fichier main : héberge généralement une fonction qui assemble toutes les fonctions baby step du fichier utils
    + un dossier Tests : contient les unit testings qui permettent d'évaluer les régressions et les améliorations spécifiques à la brique, indépendamment des autres. C'est un principe essentiel qui permet un debug plus rapide et plus efficace.
  • Lorsqu'on travaille sur des algorithmes de machine learning, il est préférable de stocker les weights potentiels à la racine du projet, puisqu'ils peuvent être remplacés très souvent au cours du développement.
  • L'essai de nouvelles features peut facilement se faire à l'aide de notebooks. Étant donné la structure, chacun devrait contenir le code Python suivant pour pouvoir « voir » et importer le module counthighwaypy :

import os
import sys
sys.path.insert(0, os.path.abspath("../")) #visible parents folders
from counthighwaypy import ...
### your code
  • Au sein du module counthighwaypy, il est important d'avoir un fichier utils, un conf et un fichier main qui orchestre les différentes briques, sans oublier les Tests E2E
  • Le fichier conf est très essentiel car il définit la racine du projet et ses différents sous-modules et dossiers. Il peut être écrit comme suit :
import os
PROJECT_ROOT = os.path.realpath(os.path.join(os.path.realpath(__file__), "../.."))
##### directories
DATA = os.path.join(PROJECT_ROOT, "data/")
NOTEBOOK= os.path.join(PROJECT_ROOT, "notebooks")
SRC = os.path.join(PROJECT_ROOT, "counthighwaypy/")
WEIGHTS = os.path.join(PROJECT_ROOT, "weights/")
MODULE_DETECT_VEHICLE = os.path.join(SRC, "detectvehicle/")
MODULE_DETECT_LICENCE_PLATE = os.path.join(SRC, "detectlicenseplate/")
MODULE_OCR_LICENSE_PLATE = os.path.join(SRC, "ocrlicenseplate/")
  • Le fichier app encapsule votre projet dans une API consommable par d'autres utilisateurs et services
  • Des fichiers additionnels tels que packages.txt, pytest.ini, et Dockerfile sont placés à la racine du projet

Une fois la structure du code en place, il est préférable de développer chaque brique de façon autonome, indépendamment des autres. Ceci étant dit, voici quelques recommandations à suivre :

  • Définissez le format de l'entrée et de la sortie de chaque brique, où la sortie de la brique i est l'entrée de la brique i+1
  • Écrivez le canevas du code (fonctions vides) de manière simple : à la lecture, on comprend immédiatement ce que fait le script
  • N'oubliez pas les signatures et les commentaires
  • Utilisez un outil de versionnement de code, git par exemple, pour une collaboration plus efficace
  • Gardez votre code propre à l'aide de code formatters et code linters
  • Pour la collaboration cross-team, exposez votre code/package en tant qu'API consommable

Imports en Python

Depuis Python 3.3, un dossier folderame est considéré comme un module (sans besoin de fichier __init__.py) et peut être simplement importé dans un fichier Python, à condition qu'il soit visible, c'est-à-dire au même niveau de l'arborescence, en utilisant :

import foldername

Supposons que nous ayons la structure suivante :

|--FOLDER1/ |--|--file1.py|--FOLDER2/ |--|--file2.py|--main.py
  • Dans main.py on peut
import FOLDER1.file1
import FOLDER2.file2
  • Pour importer file2 dans file1 :
import os
import sys
#make FOLDER2 visible to file1 (one step up in the tree)
sys.path.insert(0, os.path.abspath("../"))
from FOLDER2 import file2

Dans un projet Python complexe, pour garder vos imports cohérents, il est recommandé de tous les commencer depuis la source de votre code. Dans notre cas, commencez tous vos imports dans n'importe quel fichier .py par :

from counthighwaypy.xxx.xxx import xxx

Conclusion

Il existe d'autres façons de structurer votre projet Python, mais je trouve celle décrite dans cet article simple à comprendre et facile à suivre. Elle peut aussi être appliquée à d'autres langages que Python.

J'espère que vous avez apprécié la lecture de cet article et qu'il vous aidera à mieux organiser votre travail à l'avenir.
Tous les commentaires et suggestions sont les bienvenus !

En tant que développeur, je trouve que coder est un art. Lorsqu'on travaille sur un projet complexe, il y a de nombreuses étapes à suivre pour développer un code cohérent, solide et durable, qui pourra être lu et repris par d'autres contributeurs :

Restons en contact

Vous avez une question ? Nous serions ravis d'échanger avec vous.