Améliorez votre code

améliorer votre code sur plusieurs dimensions: une installation locale mais universelle (environnement et requirements), un format standard (black), pour un code de qualité lisible et sans bug apparent (lint) et une centralisation des principales commandes (Makefile).

Plan du chapitre

Ressources

Chapitre 4: organisation du code

Dans le dernier chapitre vous êtes passé d’un notebook simple, personnel à un code sur github auquel peuvent contribuer les collaborateurs ayant accès. Vous avez veillé à fournir les instructions nécessaires pour installer, reproduire et tester votre code dans le README et spécifié dans le .gitignore les fichiers à exclure de github.

Le code de point de départ du chapitre est disponible dans la branche chapitre_4 du github du projet MLOps-DPE.

Dans ce chapitre nous continuons dans cette voie pour améliorer votre code sur plusieurs dimensions: une installation locale mais universelle (environnement et requirements), un format standard (black), pour un code de qualité lisible et sans bug apparent (lint) et une centralisation des principales commandes (Makefile).

  1. Spécifier les packages du projet dans un fichier requirements.txt

  2. Définir un environnement en local (venv, conda ou poetry)

  3. Formater le code avec Black

  4. Améliorer la qualité avec pylint

  5. Centraliser toutes ces opérations avec un Makefile

A la fin de ce chapitre votre repo sera prête pour décoller.!

Spécifier les librairies avec requirements.txt

La reproductibilité étant à la base du MLops, il faut que votre alter ego ou votre successeur puisse exécuter votre code sur sa machine. Mais votre environnement en termes de librairies et de leurs versions ne sera sans doute pas les mêmes que les siennes. Vous travaillez par exemple en python 3.10 et avec une version de scikit learn qui date un peu tandis que votre alter ego est déjà en python 12 et possède 5 versions de scikit learn installé sur son ordi.

Vous allez donc spécifier les versions des principales librairies dans un fichier dédié appelé requirements.txt.

Votre alter ego pourra ensuite installer le même environnement que vous avec la commande

pip install -r requirements.txt

simple!

La commande

pip freeze > requirements.txt

va enregistrer toutes vos librairies et leur version dans le fichier requirements.txt

Vous n’avez évidemment pas besoin de toutes les librairies et leur dépendances installées en local pour notre projet. Vous pouvez alors simplifier le fichier en ne gardant que les principales librairies.

Dans notre contexte, il nous suffit d’avoir pour l’instant que les librairies suivantes scikit-learn, pandas et numpy.

Notre fichier requirements.txt va donc contenir les lignes suivantes: (notez le double == entre le nom de la librairie et sa version)

scikit-learn==1.3.2
pandas==2.1.4
numpy==1.26.3

Les versions de librairies dans cet exemple sont arbitraires. Ces numéros seront probablement différents dans votre environnement. Le tout c’est qu’il n’y ait pas de conflit entre les versions des librairies quand vous les installerez.

Note: pour connaître la dernière version disponible d’une librairie python, allez sur le site pypi.org et recherchez la librairie en question. Par exemple pour scikit learn il est indiqué que la dernière version à l’instant ou j’écris est 1.4.2

Enter image alt description

Je vais donc mettre à jour ma version de scikit learn avec

pip install --upgrade scikit-learn==1.4.2

et spécifier dans mon fichier requirements.txt

scikit-learn==1.4.2
pandas==2.1.4
numpy==1.26.3

TODO: test si nouvelle version dans fichier mais pas upgradé, est ce que ca installe la dernier version aussi ?

Voir cet article “Pip install a specific version of a Python package” si vous souhaitez en savoir plus.

Version locale:

Vous pouvez obtenir directement les versions des librairies installées en local avec la commande

pip freeze | grep <nom ou partie du nom de la librairie>

Par exemple pour scikit-learn, ne sachant plus quelle est le nom exacte de la librairie (sklearn, scikitlearn, scikit-learn), j’ai exécuté

pip freeze | grep sci

avec pour résultat

scikit-learn==1.3.2
scipy==1.10.1

grep

Note:: dans la commande pip freeze | grep sci

pip freeze va afficher toutes les librairies installées sur votre local.

le ` ` aussi appelé pipe va rediriger cette liste vers la prohaine commande (grep)

grep prends le resultat de pip freeze et ne retourne que les lignes qui contiennent la string “sci”

grep est surtout tres utile pour trouver les lignes contenant une string particuliere dans un fichier

par exemple trouver toutes les lignes de définition de fonction dans un fichier python:

grep “def “ main.py

vous donnera la liste des fonctions du fichier

Donc votre alter ego peut maintenant reproduire votre environnement et avoir les mêmes versions des librairies. super! Par contre, installer vos versions risque d’écraser les siennes et de causer des problèmes au final.

Créer un environnement virtuel par projet

Pour éviter cela, on crée un unique environnement de developpement par projet.

Il y a 3 outils: conda, poetry et le plus simple pip

Vous pouvez utiliser des outils comme conda ou poetry. Mais le plus simple reste encore de créer un environnement virtuel avec venv.

La commande python -m venv venv crée un répertoire venv dans lequel seront installées les bonnes versions des librairies sans empiéter sur votre installation par défaut, hors environnement virtuel. Vous pouvez utiliser un autre nom pour votre environnement que venv. Par exemple alphonse, aglaé ou sidonie.

Nul n’est besoin de créer ce répertoire venv au sein du projet mais c’est plus simple pour partager l’environnement dans plusieurs projets.

Dans ce cas attention à bien ajouter le répertoire venv à votre .gitignore autrement toutes les librairies qui s’y trouvent seront déployées inutilement sur votre repo git.

Pour commande en ligne
Créer un environnement nommé venv python -m venv venv
Activer: Unix ou MacOS source venv/bin/activate
Activer sur Windows (cmd.exe) venv\Scripts\activate.bat
Activate sur Windows (PowerShell) venv\Scripts\Activate.ps1
Désactiver l’environnement deactivate

Que faire en cas de conflit entre les versions ?

Il n’y a malheureusement pas de commande magique pour désinstaller les librairies qui poseraient problème. La bonne stratégie est désinstaller les librairies posant problème jusqu’à ce qu’il n’y ait plus de conflit

puis installer à nouveau les packages un à un.

pip uninstall <nom du package>

Les erreurs de conflit sont assez explicites en ce qui concerne les versions des librairies. En regardant bien vous devriez pouvoir mettre à jour ou downgrader les versions incompatibles.

Nous avons en partie résolu les frictions entre environnement et librairies. Tournons nous maintenant vers le code et la contrainte de faire tenir chaque ligne en 80 caractères. N’est ce pas un peu court ?

Un bon format facilite la vie de tous

En informatique le standard est d’avoir un maximum de 80 caractères par ligne de code. Pourquoi un chiffre si petit ? Et pourquoi celui-là ?

Cette norme remonte a l’utilisation des cartes perforées brevetées par IBM en 1920, soit il y a plus de 100 ans. Lisez à ce sujet cet article wikipédia c’est fascinant. https://fr.wikipedia.org/wiki/Carte_perfor%C3%A9e#Carte_IBM_[%C3%A080_colonnes](https://fr.wikipedia.org/wiki/Carte_perfor%C3%A9e#Carte_IBM%C3%A0_80_colonnes)

Les premiers écrans ont donc adopté 80 caractères comme largeur standard pour être compatible avec ces cartes. Et certains systèmes (peu utilisés certes) continuent d’utiliser 80 caractères par ligne.

Enter image alt description

Exemple de carte perforée IBM Par Mutatis mutandis — Travail personnel, CC BY 2.5, https://commons.wikimedia.org/w/index.php?curid=1406914

Mais contraindre une ligne entre 70 et 120 caractères facilite aussi la lecture du code. Avec une ligne trop longue, on oublie le début tandis qu’avec une ligne trop courte il faut constamment passer à la ligne pour continuer la lecture. 80 signes est un bon juste milieu.

D’un point de vue logiciel, éviter de combiner plusieurs opérations sur une même ligne rend le code plus lisible et compréhensible. Cela facilite aussi le git diff qui souligne les différences par ligne.

La longueur de la ligne n’est pas le seul élément de standardisation de code. Il existe des standards d’écriture de code très complets. Le plus classique est PEP8 écrit en 2001 par Guido van Rossum, le créateur de python. La question brûlante de l’utilisation des tabulations ou des espaces y figure en bonne place. (espaces oui et tabulations non)

Mais comme vous n’allez pas inspecter votre code ligne par ligne pour vérifier son format en fonction des règles de PEP8, la communauté python a développé un outil de formatage automatique : Black.

Black formate automatiquement vos fichiers python en respectant le standard PEP8.

Le but officiel de Black est de créer les diffs les plus petites pour faciliter les codes reviews.

Le détail des changements effectués par Black est explicité dans la documentation Black.

Pour installer Black:

pip install black

Pour appliquer Black sur les fichiers python du folders src (avec une largeur de ligne de 100 caractères)

black -l 100 src/*.py

On obtient:

Enter image alt description

Utiliser Black pour formater votre code, non seulement juste avant de le commiter sur github mais aussi dans votre flow continue de développement est une excellente habitude à prendre.

Le linting pour améliorer votre code

Formater le code c’est bien, mais s’assurer de sa cohérence c’est encore mieux.

Un code de qualité est un code cohérent dans son écriture, la déclaration et la nomination de ses variables, dans l’ordre des imports brefs tout ce qui gomme les aspérités et permet de débusquer les bugs.

La librairie Pylint est un analyseur statique de code qui va repérer des erreurs de programmation ou de conception et pour déterminer la facilité ou la difficulté à maintenir le code. Pylint va ensuite suggérer des améliorations pour rendre votre code plus robuste.

En pratique, pylint suit plus de 400 règles pour analyser votre code et lui donner une note sur 10. En dessous de 10/10, c’est simple votre code pourrait être de meilleure qualité.

Entre autres choses pylint va vérifier

Le terme lint veut dire peluche, non pas au sens animal en peluche, mais plutôt, peluche de la laine ou du coton. Si vous utilisez un séchoir à linge, il vous faut nettoyer régulièrement le filtre à peluche (lint trap) pour enlever les résidus de coton, laine et autres textiles qui y seraient attrapés. Le linting de votre code s’apparente au dé-peluchage de vos vêtements.

pour commande
installer pylint pip install pylint
exécuter pylint sur un répertoire pylint src/
pour éviter certaines vérification pylint -disable repeated-keyword src/

Quelques exemples

Voici quelques exemples obtenu sur le notebook du chapitre 2

de quoi s’amuser!

Au final, pylint est très utile et linter son code régulièrement et avant de le mettre sur git, est une excellent habitude à prendre. Au final, vous y gagnerez du temps. , les autres développeurs ne vous en seront que reconnaissant et votre code sera de meilleure facture.

Note: Il est possible de supprimer certaines vérifications en écrivant directement dans le fichier python sous la forme d’un commentaire.

# pylint: disable=not-callable, fixme, no-member

Pour en savoir plus sur le linting voici un excellent article en français sur le blog mindsers.

Regrouper les principales commandes dans un Makefile

Vous avez donc à ce stade plusieurs ligne de commande à exécuter

Au début:

au fil de votre travail:

vous aurez sans doute aussi à :

Chaque commande nécessite parfois plusieurs paramètres. À la longue, cela fait beaucoup de lignes de commande à mémoriser. (non, revenir en arrière dans le terminal … ne compte pas)

Nous allons donc regrouper toutes ces commandes dans un fichier unique que l’on appelle Makefile

Dans l’univers du langage C et plus généralement des langages compilés, make est un outil utilisé pour compiler les programmes, créer les exécutables. Python étant un langage interprété, pourquoi utiliser make dans le cadre d’un projet python ? Simplement pour faciliter la tâche des développeurs en évitant de devoir mémoriser une longue liste de commandes.

En fait, en python, l’étape de compilation est implicite. Il n’y a pas besoin d’invoquer un compilateur pour faire tourner le code. Cependant lorsque l’on importe une librairie, cette dernière est compilée en bytecode dans les répertoires pycache pour accélérer son exécution.

make pour python sert à simplifier l’utilisation et la mémorisation des lignes de commandes liées au projet.

Utiliser make dans le cadre d’un projet python consiste à déclarer dans un fichier intitulé Makefile, la liste des commandes usuelles. Vous n’aurez alors qu’à exécuter

make lint

au lieu de

python -m pylint --disable C0116 src/*.py
make setup

au lieu de

source venv/bin/activate
pip install -r requirements.txt

Mon premier Makefile

Écrivons maintenant une première commande make. Dans un fichier appelé Makefile, écrivez les lignes suivantes:

hello:
	echo "Hello World"

et dans le terminal, exécutez

make hello

ce qui retourne … Hello World … C’est magique!

dans un fichier Makefile, chaque règle correspond à une ligne de commande et est composée de

rule : prérequis
       la recipe: ligne de commande a executer

et s’exécute dans le terminal avec

make rule

Attention la ligne pour la règle (recipe) est précédée d’une tabulation et non pas d’espaces

Les prérequis sont les fichiers nécessaires à la commande. Par exemple pour la règle setup on a

setup: requirements.txt
      pip install -r requirements.txt

pour indiquer que la règle setup dépend du fichier requirements.txt

Les prérequis peuvent aussi désigner une règle antérieure. Par exemple, télécharger un modèle ou un jeu de données avant de faire des prédictions.

Regroupez maintenant les différentes lignes de commandes que nous avons vu jusqu’à maintenant en définissant pour chacune un nom de commande simple

format:
	black -l 100 *.py
lint:
	pylint -disable... src/*.py

setup: requirements.txt
      pip install -r requirements.txt

Les différents operations du code sont alors

clean_data:
	python src/features.py
train_model:
	python src/train.py

Il est possible de complexifier le Makefile avec des variables, etc … je crois que la valeur justement du Makefile est de rester simple. Je vous conseille ces articles pour aller plus loin

Conclusion

Vous vous rappelez du notebook monolithique ?

Votre repo est maintenant sur github et bien mieux équipée avec du format de la qualité du code, des commandes prédéfinies dans le Makefile et une liste précise des librairies,

Le projet a la structure suivante

/data

/src

.gitignore

Makefile

readme.md

requirements.txt

A ce stade vous pensez probablement que même simplifié, exécuter make black, make lint etc … a chaque fois que vous souhaitez transférer du code sur github n’est pas agreable. vous allez sans aucun doute en oublier une partie.

C’est là que le CI/CD ou continuous integration / continuous delivery apparaît comme par magie.