Outils et bonnes pratiques pour un code python de bonne qualité

  • Ahmed 

Introduction

Lorsqu’on écrit du code, le réflexe premier est de le faire fonctionner. Cependant, travailler dans une équipe de développeurs professionnels présente une pléthore de défis.

En trois articles successifs, nous allons tenter d’adresser trois de ces défis :
Partie 1 – Outils et bonnes pratiques pour bien coder en python.
Partie 2 – Intégration Continue dans Python.
Partie 3 – Outils de monitoring, de gestion de log ainsi qu’au calcul de métriques.

Dans cet article, nous allons donc introduire les bonnes pratiques et des règles à respecter pour écrire un code Pyhon de qualité. Nous allons également, avec des exemples simples, illustrer l’usage d’outils permettant de contrôler le respect de ces règles.

Qualité de code?

Plusieurs définitions existent, mais la plupart s’accordent sur ce qui suit :

  • Il fait ce qu’il est supposé faire : s’il ne fait pas cette exigence de base, il ne sert à rien de parler de qualité de code.
  • Il ne contient pas de défauts ou des bugs : comme n’importe quel produit, avant d’être industrialisé, il faut gommer tous les défauts indésirables. En d’autres termes, pour parler de haute qualité, il faut que votre application prévoie tous les cas de figure (supposée traiter), et être capable de les adresser.
  • Il est facile à lire, à entretenir et à étendre : Personne ne veut être dans la position où il doit lire, maintenir ou étendre un code de mauvaise qualité. Cela signifie plus de maux de tête et plus de travail pour tout le monde.

Comment améliorer la qualité de code Python?

Un bon style de développement avec la PEP

Pour ceux qui ne sont pas familiers avec les PEP (Python Enhancement Proposal : proposition d’amélioration de Python), il s’agit de propositions écrites par la communauté, conçues pour améliorer certains aspects de Python, allant des performances aux nouvelles fonctionnalités, en passant par la documentation.

De la bonne syntaxe avec la PEP 8

La huitième proposition (PEP 8) fournit des conventions de codage pour le code Python. Il est assez courant que le code Python respecte ce guide de style. C’est un bon endroit pour commencer car c’est déjà bien défini. Voici certains sujets que je considère les plus importants à connaitre de la PEP8:

  • Constantes : Sont écrites en majuscule. Ex: My_CONST = ‘…’.
  • Fonctions / Variables : En minsicule avec éventuellement des underscore. Ex : my_function(), my_variable
  • Arguments de fonction / méthode : minsicule et underscore si besoin (snake case). Ex: unit_price
  • Classes / Exception : Doit utiliser CapWords(upper camel case). Recommandé de ne pas utiliser le mot Classe dans le nom. Ex: MyClasse, MyException.
  • Packages /Modules : Doit utiliser uniquement des minuscules. Les traits de soulignement peuvent être utilisés si nécessaire, mais sont déconseillés. Ex : package ou module.py
  • Indentation : 4 espaces pour bien séparer les blocs et avoir un code lisible (pas de tabulation). Pour cela, il faut régler l’éditeur de code, pour faire en sorte que lorsqu’on presse la touche tabulation, cela ajoute 4 espaces et non un caractère tabulation).
  • Longueur de ligne : Pas plus de 79 caractères par ligne
  • Ligne vides : utile pour séparer les différentes parties du code. Il est recommandé de :
    • Laisser 2 lignes vides avant la définition d’une classe.
    • Laisser 1 seule ligne vide avant la définition d’une méthode dans une classe
    • On peut aussi laisser une ligne vide dans le corps d’une fonction pour séparer les sections logiques de la fonction, mais cela est à utiliser avec parcimonie.
  • Commentaires : Commencent par le symbole #. Phrase complète en anglais de préférence. Eviter les commentaires sur la même ligne du code. Ex :
#My comment
X = x + 1

Pour plus d’informations, vous pouvez consulter : https://www.python.org/dev/peps/pep-0008/

PEP 257 et les docstrings (chaînes de documentation)

Une proposition décrivant les conventions relatives aux docstrings de Python, qui sont des chaînes destinées à documenter des modules, des classes, des fonctions et des méthodes. En voici un résumé succinct.
Entourer les docstring de triples doubles guillemets :

# A comment in a single line
"""A clear description of function in one line."""
# A comment in multiple lines
"""first line
second line
last line
"""

Notons que les docstrings sont destinées aux utilisateurs des modules, fonctions, méthodes et classes que nous développons. Les éléments essentiels pour les fonctions et les méthodes sont :

  • ce que fait la fonction, méthode, le module, …
  • ce qu’il prend en argument,
  • ce qu’il renvoie.

La PEP 257 n’exige pas un style particulier. NumPy Style Python Docstring, très utilisé en analyse de données, a un style que je trouve très intéressant.
Exemple:

def function_with_types_in_docstring(param1, param2):
    """Example function with types documented in the docstring.

    Parameters
    ----------
    param1 : int
        The first parameter.
    param2 : str
        The second parameter.

    Returns
    -------
    bool
        True if successful, False otherwise.

    .. _PEP 484:
        https://www.python.org/dev/peps/pep-0484/

    """

Si les docstrings sont cohérents, il existe des outils capables de générer de la documentation directement à partir du code.
Pour plus d’informations, vous pouvez consulter : https://www.python.org/dev/peps/pep-0257/

Tous ces guides (PEP8, PEP257) définissent un moyen de styliser le code. Mais comment l’appliquons-nous ? Et qu’en est-il des défauts et des problèmes dans le code, comment pouvons-nous les détecter ? C’est là que les outils de contrôle de qualité de code (Linters) entrent en jeu.

Outils de contrôle de la qualité du code (Linter)

Pour évaluer la qualité d’un code Python, c’est-à-dire sa conformité avec les recommandations de la PEP 8 et de la PEP 257, on peut utiliser des outils automatisés dédiés tels que pycodestyle, pydocstyle et pylint. Pour ce faire on peut utiliser la commande:

pip install pycodestyle pydocstyle pylint

En outre, la plupart des IDE ont déjà des linters intégrés. En voici quelques exemples :

  • Sublime Text : [http://www.sublimelinter.com/en/stable/linter_settings.html#python](http://http://www.sublimelinter.com/en/stable/linter_settings.html#python)
  • Atom: [https://atom.io/packages/linter-python-pep8](http://https://atom.io/packages/linter-python-pep8)
  • PyCharm: [https://plugins.jetbrains.com/plugin/11084-pylint](http://https://plugins.jetbrains.com/plugin/11084-pylint)

Par ailleurs, il existe les combo-linters (une composition de plusieurs linters), comme Flake8 ou Pylama. Ce dernier intègre un grand nombre de linters dont les trois cités ci-dessus.

Enfin, on trouve également des outils sympas pour mieux formater le code comme Black (https://github.com/python/black) ou isort (https://github.com/timothycrosley/isort). Si vous utilisez PyCharm ou Atom, les pluggins dédiés sont disponibles.

def make_batches(items, batch_size):
    Current_Batch = []
    for item in items:
        Current_Batch.append(item)
        if len(Current_Batch) == batch_size:
            yield current_batch
            
            current_batch = []
    
    yield current_batch

print(list(make_batches([1, 2, 3, 4, 5], batch_size=2)))

Après avoir corrigé les différentes anomalies syntaxiques, on obtient un code conforme à la PEP:

"""This is a test module."""


def make_batches(items, batch_size):
   """Generate a list of mini batches.

    Parameters
    ----------
    items : list
            list of lists. Each item is an int.
    batch_size : int
            size of each generated mini batch.
    Return
    ------
    list:
        list of mini batch having batch_size
         list(make_batches([1, 2, 3, 4, 5], batch_size=2))
        [[1, 2], [3, 4], [5]]
		"""
    
    current_batch = []
    for item in items:
        current_batch.append(item)
        if len(current_batch) == batch_size:
            yield current_batch

            current_batch = []

    yield current_batch


print(list(make_batches([1, 2, 3, 4, 5], batch_size=2)))

Génération de la documentation avec Pdoc

Tout d’abord, il faut installer pydoc :

pip install pdoc

Ensuite lancer la commande :

python -m pydoc batch_example doc.txt

Le contenu de doc.txt sera le suivant:

NAME
batch_example - This is a test module.

FUNCTIONS
    make_batches(items, batch_size)
        Generate a list of mini batches.

        Parameters
        ----------
        items : list
                list of lists. Each item is an int.
        batch_size : int
                size of each generated mini batch.
        Return
        ------
        list:
            list of mini batch having batch_size

Notes:

  • Pour une documentation plus sophistiquée (fichier md, html, …etc), on peut utiliser un autre module Sphinx ([http://www.sphinx-doc.org/en/master/index.html](http://http://www.sphinx-doc.org/en/master/index.html)).
  • Un bon tutoriel pour démarrer Sphinx : [https://medium.com/@richdayandnight/a-simple-tutorial-on-how-to-document-your-python-project-using-sphinx-and-rinohtype-177c22a15b5b](http://https://medium.com/@richdayandnight/a-simple-tutorial-on-how-to-document-your-python-project-using-sphinx-and-rinohtype-177c22a15b5b)
  • Pour aller plus loin dans la documentation python : [https://realpython.com/documenting-python-code/](http://https://realpython.com/documenting-python-code/)

Un développement piloté par le comportement (BDD) et par les tests (TDD)

De manière synthétique, le BDD sert à formuler les comportements (fonctionnalités) qu’il faut coder. Et le TDD sert à écrire les solutions à ces comportements.

En effet, le behavior driven development (BDD) sert à répondre à la question : « Quel est l’objectif recherché ? ». Pour s’y faire, on utilise des mots usuels dans la vraie vie comme :
Étant donné, Quand, Alors et Et.
Cette formulation va décrire le comportement de/des fonctionnalités que l’on recherche.

Exemple : Spécification Pile

  • Etant donnée une pile,
  • quand une nouvelle pile est créée
  • alors elle est vide
  • et lorsqu’un élément est ajouté à la pile, cet élément se trouve en haut de la pile.

Le test driven development (TDD) est une approche qui place les tests au cœur de notre travail de développeur. Il y a trois règles de TDD à respecter, selon Robert Martin (un leader dans le monde de TDD) :

  1. On doit écrire un test qui échoue avant d’écrire votre code lui-même.
  2. On ne doit pas écrire un test plus compliqué que nécessaire.
  3. On ne doit pas écrire plus de code que nécessaire, juste assez pour faire passer le test qui échoue.

Ces 3 règles fonctionnent ensemble et font partie d’un processus qui s’appelle le cycle rouge-vert-refactor (« red-green-refactor » en anglais).

Exemple :
Dans l’exemple Sample Factorization Matrix, nous avons installé et utilisé pytest :

pip install pytest

pour tester unitaires les cinq fonctions du module matrixfactorization.py.

(reco) C:\recommendationtool>;pytest test_matrixfactorization.py
========================= test session starts =========================================
platform win32 -- Python 3.7.1, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\recommendationtool
plugins: cov-2.7.
collected 5 items

test_matrixfactorization.py .....                                                [100%]
============================ 5 passed in 0.33 seconds =================================
(reco) C:\recommendationtool

Puis-je utiliser le TDD tout le temps?

Nous pensons que la réponse est Non. La phase exploratoire des projets de data science est un exemple de cas où le TDD n’a pas vraiment de sens. Une fois cette étape passée, la mise en place de BDD et de TDD peut aider à résoudre les problèmes plus efficacement. Avoir une idée précise de ce qu’on attend de vous en termes de fonctionnalités et de la structure du code que vous souhaitez est un processus qui peut vous faire gagner beaucoup de temps

Métrique et analyse de la complexité du code

C’est un sujet très vaste qu’on ne va pas approfondir ici. Il sera l’un des éléments qui seront adressés dans la partie 3.
Brièvement, l’analyse de la complexité et le calcul des métriques du code permettent aux développeurs de rechercher les zones problématiques dans ce code. Elles doivent faire l’objet d’une refactorisation. En outre, certains paramètres, tels que la dette technique, aident les développeurs à communiquer à un public non technique les raisons pour lesquelles des problèmes surviennent dans un système.

Radon est un outil permettant d’obtenir des mesures telles que le nombre de lignes, la complexité cyclomatique, des métriques Halstead et des métriques de maintenabilité.

Pour notre exemple précédent, le test de la complexité cyclomatique affiche un très bon score pour chacune des fonctions (ie, classe A).

(reco) C:\recommendationtool>radon cc matrixfactorization.py
matrixfactorization.py
    M 18:4 MatrixFactorization.__init__ - A
    C 10:0 MatrixFactorization - A
    M 62:4 MatrixFactorization.train - A
    M 82:4 MatrixFactorization.mse - A
    M 96:4 MatrixFactorization.sgd - A
    M 123:4 MatrixFactorization.get_rating - A
    M 135:4 MatrixFactorization.full_matrix - A

Quand puis-je vérifier la qualité de mon code ?

De manière générale, la qualité de code est vérifiée à l’une des trois situations suivantes :

Lorsqu’on écrit le code

On peut utiliser des linters au fur et à mesure qu’on écrit du code, mais ceci nécessite de configurer votre environnement.

Avant de faire un Check In du code

Si on utilise Git, on peut configurer GIT hooks pour exécuter nos linters avant de faire un commit. On peut ainsi bloquer tout nouveau code qui ne répond pas aux normes de qualité.

Bien que cela puisse sembler radical, imposer à chaque partie du code une analyse du Linter est une étape importante pour assurer une qualité continue. Automatiser ce filtrage à l’entrée principale de votre code peut être le meilleur moyen d’éviter un code non respectueux aux recommandations de la PEP 8 et de la PEP 257.

Lorsqu’on exécute des tests

Il est également possible de placer des Linters directement dans le système utilisé pour l’Intégration Continue (ex : Jenkins, Travis-CI, Tox, gitLab-CI, …Etc). Les linters peuvent être configurés pour échouer à la construction si le code ne répond pas aux normes de qualité exigées du Linter.

Cela peut sembler une étape radicale, surtout s’il existe déjà de nombreuses erreurs de linter dans le code existant. Pour lutter contre cela, certains systèmes d’IC permettent de ne pas réussir la construction si le nouveau code augmente le nombre d’erreurs linter déjà présentes. De cette façon, on peut commencer à améliorer la qualité sans avoir à réécrire entièrement notre code existant.

Conclusion

Dans cet article nous avons appris comment écrire du code Python lisible en appliquant les bonnes pratiques et directives du PEP (8 et du PEP 257). Nous avons également vu brièvement comment utiliser des linters, des techniques telles que le BDD, TDD et l’analyse de complexité.

Bien que ces instructions puissent sembler déroutantes, les suivre améliore très significativement la qualité du code et facilite le travail en équipe.

Pour avoir un code python qui vit bien dans son écosystème, nous allons aborder dans les deux parties suivantes de cette série Python et ses bonnes pratiques les sujets suivants:

  • Partie 2 : L’Intégration Continue avec Python : Où nous parlerons de GitLab et GitLab-CI.

  • Partie 3 : Monitoring, Log et calcul de métrique avec Python (Radon, Elastic APM, Promethus, Sentry, DataDog, …Etc).

Stay tuned !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.