Améliorer son code Python

La syntaxe de Python est très permissive. Pour uniformiser l’écriture de code, la communauté des développeurs Python recommande un certain nombre de règles afin qu’un code soit lisible. Lisible par vous mais également par d’autres. Nous allons voir dans ces lignes quelles sont ces règles.

Essayez de relire un code que vous avez écrit «rapidement» il y a un mois, six mois ou un an. Si le code ne fait que quelques lignes, il se peut que vous vous y retrouviez, mais s’il fait plusieurs dizaines voire centaines de lignes, cela risque d’être différent. Le créateur de Python, Guido van Rossum, le dit simplement : « Code is read much more often than it is written », le code est plus souvent lu qu’écrit. Voyons en quoi consistent ces bonnes pratiques. Les développeurs parlent de code pythonique lorsque ce dernier respecte les règles d’écriture définies par la communauté Python mais aussi les règles d’usage du langage. Plusieurs choses sont nécessaires pour écrire un code lisible : la syntaxe, l’organisation du code, le découpage en fonctions et en classes. Pour cela, les PEP peuvent nous aider.

Les PEP

Afin d’améliorer le langage, la communauté Python publie régulièrement des PEP (Python Enhancement Proposal www.python.org/dev/peps/) numérotées. Il s’agit de propositions concrètes pour améliorer le code. La PEP 8, Style Guide for Python Code (www.python. org/dev/peps/pep-0008/), est l’une des toutes premières PEP. Incontournable dès lors que vous voulez écrire du code Python correct, elle consiste en un nombre important de recommandations sur la syntaxe du langage. Il est vivement recommandé de lire cette PEP en entier au moins une fois dans sa vie de développeur Python pour avoir une bonne vue d’ensemble. Nous allons vous en faire un rapide résumé.

Indentation

L’indentation est obligatoire en Python pour séparer les blocs d’instructions. Ce n’est pas seulement une bonne pratique destinée à améliorer la lisibilité d’un code. Dans la PEP 8, la recommandation pour la syntaxe de chaque niveau d’indentation est très simple : 4 espaces.

Importation des modules

Le chargement d’un module doit se faire avec l’instruction import module plutôt qu’avec from module import*. Pour utiliser une fonction d’un module, il vaut mieux écrire module. fonction(), ce qui précise explicitement la provenance de la fonction plutôt que fonction() tout court. Cette deuxième syntaxe peut mener à un conflit si au moins deux fonctions de modules distincts ont le même nom.

Qui plus est, cela rend la recherche de documentation plus difficile. Les modules doivent être importés ligne par ligne, en commençant par les modules internes (classés par ordre alphabétique), c’est-à-dire les modules de base de Python, puis les modules externes – les autres. Si le nom d’un module est trop long, vous pouvez utiliser un alias (as …). L’instruction from doit être réservée à l’import de fonctions clairement identifiées. Cela donne, en résumé :

import module_interne_n1

import module_interne_n2

from module_interne_n3 import fonction_spécifique

from module_interne_n4 import constante_1, fonction_1, fonction_2

import module_externe_n1

import module_externe_n2

import module_externe_n3_qui_a_un_nom_tres_long as mod3         # mod3 est un alias

Règles de nommage

Les noms de variables, de fonctions et de modules doivent être de la forme :

age_du_capitaine

calculer_age()

module_trigonometrie

C’est-à-dire en minuscules en séparant les différents mots par un caractère underscore (tiret du bas). Ce style est appelé snake_case.

Les constantes doivent être écrites en majuscules :

MONTANT_MAX

VITESSE_MINIMALE

Pour les noms de classes et d’exceptions, il faut employer le CamelCase :

ClasseUnetelle

ExceptionSpecifique

Donnez à vos variables des noms qui ont du sens. Évitez autant que possible les a1, a2, variable1, variable2, ma_variable et autres titi, tata ou tutu. Les noms de variables à un caractère sont néanmoins autorisés pour les compteurs de boucles et, du coup, les indices de tableau :

liste_valeurs = [1, 3, 5, 7, 9, 11]

for i in range(len(liste_valeurs)):

      print(liste_valeurs[i])

L’emploi d’un foreach (for in en Python) vous évitera d’utiliser des indices tant que vous vous contentez de faire défiler une liste en lecture seule, comme dans l’exemple suivant :

liste_valeurs = [1, 3, 5, 7, 9, 11]

for entier in liste_valeurs:

      print(entier)

L’autre exception possible concerne l’emploi de noms de variable à une lettre lorsqu’elles font référence à des coordonnées cartésiennes (x, y, z, t, …) ou, plus généralement, à des éléments mathématiques (paramètres d’équations, par exemple).

Gestion des espaces

La PEP 8 recommande d’entourer les opérateurs (+, -, /, *, ==, !=, >=, not, in, and, or...) d’un espace avant et d’un espace après. Par exemple :

 # code recommandé :

variable_somme = 3 + 7

nom_periph = "souris"

nom_periph == variable_somme

# code non recommandé :

variable_somme=3+7

nom_periph="souris"

nom_periph==variable_somme

Ce n'est pas le cas à l'intérieur de crochets, d'accolades ou de parenthèses :

# code recommandé :

liste_valeurs[1]

dico_clefs{"clé"}

fonction_unetelle(argument)

# code non recommandé :

liste_valeurs[ 1 ]

dico_clefs{"clé" }

fonction_unetelle( argument )

Ni non plus avant la parenthèse ouvrante d'une fonction ou le crochet ouvrant d'une liste ou d'un dictionnaire :

# code recommandé :

liste_valeurs[1]

dico_clefs{"clé"}

fonction_unetelle(argument)

# code non recommandé :

liste_valeurs [1]

dico_clefs {"clé"}

fonction_unetelle (argument)

Vous mettrez un espace après les caractères : et , mais pas avant :

# code recommandé :

liste_valeurs = [1, 2, 3]

dico_clefs = {"clé1": "valeur1", "clé2": "valeur2"}

fonction_unetelle(argument1, argument2)

# code non recommandé :

liste_valeurs = [1 , 2 ,3]

dico_clefs = {"clé1":"valeur1", "clé2":"valeur2"}

fonction_unetelle(argument1 ,argument2)

Par contre, pour les plages dans les listes, on ne met pas d'espace autour du signe :

liste_valeurs = [1, 3, 5, 7, 9, 1]

# code recommandé :

liste_valeurs[1:3]

liste_valeurs[1:4:2]

liste_valeurs[::2]

# code non recommandé :

liste_valeurs[1 : 3]

liste_valeurs[1: 4:2 ]

liste_valeurs[ : :2]

Vous n'ajouterez pas non plus plusieurs espaces autour du symbole = ou des autres opérateurs pour « faire joli » ou indenter bizarrement :

# code recommandé :

x1 = 10

x2 = 30

x_ter = 25

# code non recommandé :

x1           = 10

x2           = 30

x_ter     = 25

SublimeLinter est un plugin pour l’IDE Sublime Text fournissant un framework de contrôle (linting) du code.

Longueur de ligne

Une ligne de code ne doit pas dépasser l’écran « classique » (environ 80 caractères, mais cela peut varier selon vos paramètres d’affichage) pour des raisons assez évidentes de lisibilité, de relecture rapide du code.

Le caractère \ (antislash) permet de couper des lignes trop longues. Par exemple :

variable_somme = 3

if variable_somme > 1 and variable_somme < 10 \

and variable_somme % 2 == 1 and variable_somme % 3 == 0:

print("variable_somme vaut {}".format(variable_somme))

variable_somme vaut 3

À l'intérieur d'une parenthèse, vous pouvez passer à la ligne suivante sans utiliser le caractère \. C'est particulièrement utile pour préciser les arguments d'une fonction ou d'une méthode lors de sa création ou de son utilisation :

def fonction_unetelle(argument_1, argument_2,

argument_3, argument_4):

return argument_1 + argument_2

fonction_unetelle("texte très long", "jaguar",

"panthère", "tigre")

'texte très long tigre'

Les parenthèses sont également très pratiques pour découper une chaîne de caractères sur plusieurs lignes :

print("Rappelez-vous l'objet que nous vîmes, "

        "mon âme, Ce beau matin d'été si doux : "

        "Au détour … "

Rappelez-vous l'objet que nous vîmes, mon âme, Ce beau matin d'été si doux : Au détour …

Il n’est alors pas nécessaire d’employer l'opérateur + pour concaténer les chaînes de caractères. Les parenthèses peuvent aussi être utilisées pour évaluer une expression trop longue :

variable_somme = 3

if (variable_somme > 1 and variable_somme < 10

and variable_somme % 2 == 1 and variable_somme % 3 == 0):

        print("variable_somme vaut {}".format(variable_somme))

variable_somme vaut 3

Les parenthèses sont aussi très utiles lorsqu'on a besoin d’enchaîner des méthodes les unes à la suite des autres. Enfin, il est possible de créer des listes ou des dictionnaires sur plusieurs lignes, en effectuant les sauts de ligne après la virgule :

liste_valeurs = [1, 2, 3,

      4, 5, 6,

      7, 8, 9]

dico_clefs = {"clé1": 13,

      "clé2": 42,

      "clé3": -10}

Lignes vides

Dans un script, les lignes vides sont indispensables pour séparer visuellement les différentes parties du code et le rendre plus lisible. Il est recommandé de laisser deux lignes vides avant la définition d'une fonction ou d'une classe et de laisser une seule ligne vide avant la définition d'une méthode de classe. Vous pouvez aussi laisser des lignes vides dans le corps d'une fonction pour séparer ses différentes sections logiques.

Commentaires

Les commentaires débutent toujours par le symbole # suivi d'un espace. Ils sont là pour donner des explications claires sur l'utilité du code et doivent être synchronisés avec les modifications qui lui sont apportées. Les commentaires doivent être sur le même niveau d'indentation que le code qu'ils commentent. La PEP 8 recommande très fortement d'écrire les commentaires en anglais. Néanmoins, la langue à employer devrait être de préférence la langue « transverse » des développeurs, donc le français si c’est le cas. Soyez cohérent concernant le choix de la langue utilisée pour les commentaires et de celle employée pour le nommage des variables. Pour un programme scientifique, les commentaires et les noms de variables seront plutôt en anglais. Ainsi liste_premiere deviendra first_list et fonction_x deviendra function_x, par exemple.

Les commentaires qui suivent le code sur la même ligne sont à éviter le plus possible et doivent être séparés du code par au moins deux espaces, comme ceci :

x = x + 100   #  End of line comment.

Si vos docstrings sont correctement écrits, des outils vous permettent de générer de la documentation directement à partir du code : www.python.org/dev/peps/pep-0257/

Les docstrings et la PEP 257

Les docstrings, que l'on pourrait traduire par « chaînes de documentation » en français, sont un élément essentiel des programmes Python. La PEP 8 et la PEP 257 ( https://www.python.org/dev/peps/pep-0257/) donnent la manière de rédiger correctement ces docstrings. En voici un résumé :

Il est nécessaire d’écrire des docstrings aussi bien pour les modules que pour les fonctions, les classes ou les méthodes. Elles peuvent être courtes et compactes et n’occuper qu’une ligne si cela suffit :

"""Docstring simple d'une ligne se finissant par un point."""

Sinon, prenez autant de lignes que nécessaire, mais restez concis et évitez le verbiage inutile.

"""Docstring de plusieurs lignes, la première ligne est un résumé.

Après avoir sauté une ligne, les détails de cette docstring sont décrits sur plusieurs lignes :

………

……..

……..

……..

Fin de la docstring avec les triples guillemets sur la ligne suivante :

"""

La PEP 257 recommande d'écrire des docstrings avec des triples doubles guillemets, c'est-à-dire

"""Cette docstring respecte la norme."""

et non de tripler les quotes ou apostrophes, comme ceci :

'''Celle-ci ne la respecte pas du tout.'''.

Google a rédigé sa propre norme spécifique de rédaction de docstrings, la Google Style Python Docstrings.

N'oubliez pas que les docstrings sont destinées aux utilisateurs des modules, fonctions, méthodes et classes que vous avez développées. Les éléments essentiels pour les fonctions et les méthodes sont :

  1. ce que fait la fonction ou la méthode,
  2. ce qu'elle prend en argument,
  3. ce qu'elle renvoie.

Pour les modules et les classes, on ajoute également des informations générales sur leur fonctionnement.

Pour autant, la PEP 257 ne dit pas explicitement comment organiser les docstrings pour les fonctions et les méthodes. Pour répondre à ce besoin, deux solutions ont émergées :

Voici un résumé de la solution NumPy avec un exemple très simple :

def multiplie_nombres(nombre1, nombre2):

    """Multiplication de deux nombres entiers.

    Cette fonction ne sert pas à grand chose.

    Parameters

    ----------

    nombre1 : int

        Le premier nombre entier.

    nombre2 : int

        Le second nombre entier.

        Avec une description plus longue.

        Sur plusieurs lignes.

    Returns

    -------

    int

        Le produit des deux nombres.

    """

    return nombre1 * nombre2

La section Parameters précise les paramètres de la fonction. Les tirets sur la ligne 7 permettent de souligner le nom de la section et donc de la rendre visible. Sont ensuite indiqués le nom et le type du paramètre séparés par le caractère deux-points. Le type n'est pas obligatoire. En dessous, une description du paramètre en question est donnée. La description est indentée. Il est fait de même pour les autres paramètres. La description de chaque paramètre peut s'étaler sur plusieurs lignes. La section Returns indique ce qui est renvoyé par la fonction (le cas échéant). La mention du type renvoyé est obligatoire. Une description de ce qui est renvoyé par la fonction est indiqué en dessous. Cette description est aussi indentée. Rédigez vos docstrings au moment où vous écrivez vos modules, fonctions, classes ou méthodes. Passer plusieurs journées à écrire les docstrings d'un gros projet peut sembler particulièrement pénible mais est néanmoins totalement indispensable.

La PEP 8 consiste en un nombre important de recommandations sur la syntaxe du langage. Tout développeur Python se doit de la lire en entier au moins une fois dans sa vie.

Outils de contrôle qualité du code

Pour évaluer la qualité d'un code Python, c'est-à-dire sa conformité avec les recommandations des deux plus importantes PEPs, la 8 et la 257, vous pouvez utiliser des sites internet ou des outils dédiés. Le site pep8online (http://pep8online.com/), par exemple, est très simple à utiliser. Il suffit de copier / coller le code à évaluer puis de cliquer sur le bouton Check code. Les outils pycodestyle, pydocstyle et pylint doivent par contre être installés sur votre machine. Avec la distribution Miniconda, cette étape d'installation se résumera à une seule ligne de commande :

conda install -c conda-forge pycodestyle pydocstyle pylint

Les outils pycodestyle, pydocstyle et pylint sont des linters, c'est-à-dire des programmes qui vont chercher les sources potentielles d'erreurs dans un code informatique. Ces erreurs peuvent être des erreurs de style (PEP 8 et 257) ou des erreurs logiques portant par exemple sur la manipulation de variables ou le chargement de modules.

Organisation du code

Il est fondamental de toujours structurer et organiser son code de la même manière. Ainsi vous saurez tout de suite où trouver telle ou telle information et les autres programmeurs pourront eux aussi retrouver plus facilement tel ou tel élément. Voici un exemple de code avec les différents éléments dans le bon ordre :

"""Docstring d'une ligne décrivant brièvement ce que fait le programme.

Usage:

======

    python nom_du_script.py argument1 argument2

    argument1: un entier signifiant quelque chose

    argument2: une chaîne de caractères décrivant autre chose

"""

__authors__ = ("Thierry Thaureaux", "Sam Suffit ")

__contact__ = ("Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser. ", "Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.")

__version__ = "1.0.0"

__copyright__ = "Creative Commons 2.0"

__date__ = "2021/03"

import module_interne1

import module_interne_2

import module_externe1

import module_externe2

CONSTANT_ONE = valeur

ANOTHER_CONSTANT = autre_valeur

class SuperClass():

    """Résumé de la docstring décrivant la classe.

    Description détaillée ligne 1

    Description détaillée ligne 2

    Description détaillée ligne 3

    Description détaillée ligne 4

    """

    def __init__(self):

        """Résumé de la docstring décrivant le constructeur.

        Description détaillée ligne 1

        Description détaillée ligne 2

        Description détaillée ligne 3

        Description détaillée ligne 4

    """

        [...]

    def simple_methode(self):

        """Docstring d'une ligne décrivant la méthode."""

        [...]

    def complex_methode(self, arg1):

        """Résumé de la docstring décrivant la méthode.

        Description détaillée ligne 1

        Description détaillée ligne 2

        Description détaillée ligne 3

        Description détaillée ligne 4

        """

        [...]

        return valeur_de_retour

def complex_function(arg1, arg2, arg3, arg4):

    """Résumé de la docstring décrivant la fonction.

    Description détaillée ligne 1

    Description détaillée ligne 2

    Description détaillée ligne 3

    Description détaillée ligne 4

    """

    [...]

    return valeur_de_retour

def simple_function(arg1, arg2):

    """Docstring d'une ligne décrivant la fonction."""

    [...]

    return valeur_de_retour

if __name__ == "__main__":

    # Début du programme principal (point d’entrée)

    [...]

Cette docstring décrit très globalement le script. Elle pourra être consultée en invoquant la commande help() si le script est importé en tant que module. Les variables commençant et se terminant par un double underscore (__) donnent des informations sur la version du script, ses auteurs, la licence et sa date de conception. Ces métadonnées pourront être affichées via la commande help(). Ces métadonnées ne sont en rien obligatoires, mais elles sont très utiles pour la gestion du code. Suivent les informations sur l’importation des modules, sur les constantes et sur la classe et ses méthodes. Comme évoqué plus haut, une ligne vide précède la description de chaque méthode. Après les classes suivent les fonctions « classiques ». Leur description est elle aussi précédée de deux lignes vides. Enfin, le test retourne vrai seulement si le script est utilisé en tant que programme. Les lignes suivantes ne seront donc pas exécutées si le script est chargé en tant que module.

La PEP 20

La PEP 20 est une sorte de recueil de réflexions plus ou moins philosophiques avec des phrases « simples » censées guider les programmeurs Python vers la lumière du code presque parfait. Elle est accessible sous la forme d'un easter egg (œuf de Pâques) ou encore « fonctionnalité cachée d'un programme ».

Le NumPy Style Python Docstrings est la norme de rédaction de docstrings de NumPy, un module complémentaire à Python très utilisé en analyse de données

Conseils sur la conception d'un script Python

La bonne conception d’un script Python sera facilitée si vous respectez certains principes élémentaires. Découpez en fonctions chaque élément atomique de votre programme. Vous pourrez ainsi tester chaque partie indépendamment du reste. Ecrivez les docstrings en même temps que votre code et mettez les deux à jour en simultané. Si l'algorithme à coder est complexe, commentez votre code de manière très détaillée pour expliquer ce que vous faites. Si vous avez besoin d’un nouveau type de donnée, créez tout simplement une classe. Utilisez des noms de variables explicites, cela fait partie de la documentation. La (re)lecture de votre code doit être la plus aisée possible. Si vous construisez une structure de données complexe comme par exemple une liste de dictionnaires (tableaux clefs-valeurs), documentez et illustrez l'organisation de cette structure de données sur un exemple simple et compréhensible par (presque) tous.


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, vous pouvez utiliser des outils automatisés dédiés tels que pycodestyle, pydocstyle et pylint. Vous pouvez utiliser pour cela la commande : pip install pycodestyle pydocstyle pylint   Rappelons aussi que la plupart des IDE Python ont déjà des linters intégrés. C’est le cas notamment de Sublime Text (http://www.sublimelinter.com/en/stable/), d’Atom (https://atom.io/packages/linter-python-pep8 ou de PyCharm (https://plugins.jetbrains.com/plugin/11084-pylint). Il existe aussi des combo-linters (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, vous pouvez trouver des outils de formatage du code tels que Black (https://github.com/psf/black) ou isort (https://github.com/PyCQA/isort/releases).

 

L’article Python Code Quality : Tools & Best Practices (https://realpython.com/ python-code-quality/) du site Real Python explore en détail la notion de qualité pour un code Python. De nombreux linters y sont présentés.