1. Qualité du code

La qualité du code est un concept essentiel en programmation, en particulier dans le développement de logiciels. Elle fait référence à la mesure dans laquelle le code source d’un programme répond à des critères de performance, de lisibilité, de maintenabilité et de sécurité.

La qualité du code est souvent évaluée en fonction de plusieurs critères, qui peuvent varier selon le contexte et les besoins spécifiques d’un projet. Cependant, il existe des caractéristiques communes qui définissent un code de haute qualité.

Un code de faible qualité satisfait aux spécifications minimales et ne contient pas d’erreurs critiques, mais il peut être difficile à lire, à comprendre et à maintenir. Il peut également être inefficace ou ne pas respecter les bonnes pratiques de programmation.

A contrario, ce qu’on attend d’un code de haute qualité :

  • fonctionnalité : il fonctionne comme prévu et remplit son objectif ;

  • efficacité : il optimise le temps d’exécution en utilisant les bibliothèques adaptées ;

  • scalabilité : il gère l’augmentation des données ou de la complexité sans perte de performance ;

  • sécurité : il protège contre les failles et les entrées malveillantes ;

  • portabilité : il fonctionne sur différentes plateformes et environnements ;

  • modularité : il est divisé en modules ou classes, facilitant la compréhension et la réutilisation ;

  • gestion des erreurs : il gère les exceptions et les erreurs de manière appropriée ;

  • style de codage : il suit un style de codage cohérent et lisible, facilitant la collaboration entre développeurs ;

  • tests unitaires : il est accompagné de tests unitaires pour vérifier son bon fonctionnement ;

  • gestion des dépendances : il utilise des bibliothèques et des dépendances de manière appropriée, en évitant les conflits et les versions obsolètes ;

  • lisibilité : il est écrit dans un style lisible et compréhensible, facilitant la collaboration entre développeurs ;

  • documentation : il est bien documenté, avec des commentaires clairs et des exemples d’utilisation ;

  • gestion des versions : il utilise un système de contrôle de version pour suivre les modifications et faciliter la collaboration entre développeurs ;

  • gestion des performances : il est optimisé pour fonctionner efficacement, en évitant les goulets d’étranglement et les ralentissements ;

  • maintenabilité : il est facile à maintenir, avec une structure claire et des conventions de nommage cohérentes.

La qualité du code est essentielle pour garantir la fiabilité, la sécurité et la performance d’un logiciel. Elle permet également de réduire les coûts de maintenance et de développement à long terme, en facilitant la compréhension et la modification du code par d’autres développeurs.

1.1. Exemples

Voilà un ensemble de codes Python présentés à la fois dans une version fonctionnelle mais naïve, mais également dans une version améliorée.

1.1.1. Typage

Ce premier exemple rappelle pour mémoire que le typage est une bonne pratique de programmation. Il est recommandé d’utiliser des annotations de type pour indiquer les types des arguments et des valeurs de retour des fonctions. Cela améliore la lisibilité du code et facilite la détection d’erreurs potentielles.

def somme(a, b):
    return a + b

def somme(a: int | float, b: int | float) -> float:
    a, b = float(a), float(b)
    return a + b

Ce concept sera détaillé dans le paragraphe Typing.

1.1.2. Self explanatory code

Une tendance récente dans le développement de logiciels est d’utiliser des noms de variables et de fonctions qui sont explicites et auto-explicatifs (ce qui implique parfois une longueur importante). Cela signifie que le nom de la variable ou de la fonction doit indiquer clairement ce qu’elle représente ou ce qu’elle fait.

def ca(w, h):
    return w * h

def calculate_rectangle_area(width: float, height: float) -> float:
    return width * height

1.1.3. Docstrings

Une bonne pratique est d’utiliser des docstrings pour documenter les fonctions. Les docstrings sont des chaînes de caractères qui décrivent ce que fait la fonction, ses arguments et sa valeur de retour. Cela facilite la compréhension du code et permet aux autres développeurs de savoir comment utiliser la fonction sans avoir à lire le code source.


>>> def multiply(a, b):
...     return a * b
...


>>> def multiply(a: float, b: float) -> float:
...     """Multiply two numbers.
...     Args:
...         a (float): First number.
...         b (float): Second number.
...     Returns:
...         float: Product of a and b.
...     """
...     return a * b

1.1.4. Multi lignes

Un autre aspect important de la lisibilité du code est la gestion des lignes longues. Il est recommandé de limiter la longueur des lignes de code pour éviter de rendre le code difficile à lire. En Python, il est courant de limiter les lignes à 79 caractères.

def calcTotal(price,taxRate=0.05): return price*(1+taxRate)

def calculate_price_with_taxes(
    base_price: float,
    tax_rate: float = 0.05
    ) -> float:
    return base_price * (1 + tax_rate)

1.1.5. Responsabilité unique

Il est fortement conseillé de structurer le code en fonctions élémentaires qui ont une seule responsabilité. Cela signifie que chaque fonction doit effectuer une tâche spécifique et ne pas être trop complexe. Cela facilite la compréhension du code et permet de le tester plus facilement.

def process(numbers):
    cleaned = [number for number in numbers if number >= 0]
    return sum(cleaned)

def clean_data(numbers: list[int]) -> list[int]:
    return [number for number in numbers if number >= 0]

def calculate_total(numbers: list[int]) -> int:
    return sum(numbers)

1.1.6. Gestion des exceptions

Les problèmes potentiels pouvant intervenir lors de l’exécution doivent être encapsulés dans des blocs try/except. Cela permet de gérer les erreurs de manière appropriée et d’éviter que le programme ne plante en cas d’erreur inattendue.

def divide_numbers(a, b):
    return a / b

def divide_numbers(a: float, b: float) -> float | None:
    try:
        return a / b
    except ZeroDivisionError:
        print("Error: can't divide by zero")

1.1.7. Tests intégrés

En toute rigueur, de la même manière qu’une fonction doit être accompagné du typage et de docstrings, elle doit également être accompagnée de tests unitaires. Ces tests permettent de vérifier que la fonction fonctionne correctement et de détecter les erreurs potentielles. Ce point sera développé dans un autre chapitre.

def greet(name):
    print(f"Hello, {name}!")

def greet(name: str) -> str:
    return f"Hello, {name}!"

def test_greet():
    assert greet("Alice") == "Hello, Alice!"

Ce concept sera détaillé dans le chapitre Tests unitaires.

1.1.8. Algorithmique

Python est un langage de haut niveau qui permet d’écrire du code concis et lisible. Cependant, il est important de garder à l’esprit que la lisibilité du code ne doit pas se faire au détriment de la performance. Il est donc recommandé d’utiliser des algorithmes efficaces et des structures de données appropriées pour optimiser le temps d’exécution.

from time import perf_counter

def fibonacci_of(n):
    if n in {0, 1}:
        return n
    return fibonacci_of(n - 1) + fibonacci_of(n - 2)

start = perf_counter()
[fibonacci_of(n) for n in range(35)]  # Generate 35 Fibonacci numbers
end = perf_counter()

print(f"Execution time: {end - start:.2f} seconds")

from time import perf_counter

cache = {0: 0, 1: 1}

def fibonacci_of(n):
    if n in cache:
        return cache[n]
    cache[n] = fibonacci_of(n - 1) + fibonacci_of(n - 2)
    return cache[n]

start = perf_counter()
[fibonacci_of(n) for n in range(35)]  # Generate 35 Fibonacci numbers
end = perf_counter()

print(f"Execution time: {end - start:.2f} seconds")

Comme on l’a vu dans le paragraphe Le décorateur lru_cache(), l’implémentation d’un cache est facilité par le décorateur @lru_cache du module functools. Il permet de mémoriser les résultats d’une fonction pour éviter de recalculer les mêmes valeurs plusieurs fois.

1.1.9. Variables inutiles

Affecter une référence à une expression qui n’est réutilisée qu’une seule fois dans la suite du code est une mauvaise pratique. Cela rend le code plus difficile à lire et à comprendre. Il est préférable d’utiliser l’expression directement dans le code, plutôt que de créer une variable intermédiaire.

def sum_even_numbers(numbers):
    even_numbers = [number for number in numbers if number % 2 == 0]
    return sum(even_numbers)

def sum_even_numbers(numbers):
    return sum(number for number in numbers if number % 2 == 0)

1.2. Typing

Le typage est un concept fondamental en programmation qui fait référence à la manière dont les données sont stockées en mémoire et manipulées dans un langage de programmation. Le typage statique et le typage dynamique sont les deux approches principales du typage dans les langages de programmation.

Le typage statique signifie que le type d’une variable est déterminé au moment de la compilation, avant l’exécution du programme. Cela permet de détecter les erreurs de type avant que le code ne soit exécuté, ce qui peut améliorer la sécurité et la fiabilité du code. Les langages à typage statique incluent Java, C, Rust…

A contrario, le typage dynamique détermine le type d’une variable au moment de l’exécution. Cela offre plus de flexibilité, mais peut également entraîner des erreurs de type qui ne sont détectées qu’à l’exécution. Les langages à typage dynamique incluent Python, Ruby, JavaScript…

Le langage Python n’implémente pas de typage statique, mais il est possible d’utiliser des annotations de type (PEP484) pour indiquer le type des variables, des arguments de fonction et des valeurs de retour. Ces annotations sont principalement utilisées à des fins de documentation et d’aide à la compréhension du code, mais elles ne sont pas strictement appliquées par l’interpréteur Python.

Pour illustrer les annotations de type, voici un exemple simple basé sur la suite de Fibonacci :

def fib(n):
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a+b

def main():
    for i in fib(10):
        print(i)

if __name__ == "__main__":
    main()

Le même code annoté, pour préciser les types des arguments et des valeurs de retour :

from collections.abc import Iterator

def fib(n: int) -> Iterator[int]:
    a, b = 0, 1
    while a < n:
        yield a
        a, b = b, a+b

def main():
    for i in fib(10):
        print(i)

if __name__ == "__main__":
    main()

1.2.1. Vérification avec mypy

Les annotations de type sont utilisées par des outils de vérification comme mypy qui analysent le code et signalent les incohérences de type. C’est également utilisé par des IDE (par exemple VS Code) pour fournir des suggestions et des vérifications de type en temps réel au moment de l’écriture du programme.

On installe mypy avec pip

$ python -m pip install mypy

On utilise mypy pour vérifier le code Python en ligne de commande. Par exemple, si on a un fichier program.py contenant du code Python avec des annotations de type, on peut exécuter mypy sur ce fichier

$ mypy --strict program.py

Note

L’option –strict active les options suivantes --warn-unused-configs, --disallow-any-generics, --disallow-subclassing-any, --disallow-untyped-calls, --disallow-untyped-defs, --disallow-incomplete-defs, --check-untyped-defs, --disallow-untyped-decorators, --warn-redundant-casts, --warn-unused-ignores, --warn-return-any, --no-implicit-reexport, --strict-equality, --extra-checks.

Consultez la documentation pour plus d’informations.

1.2.2. Exercice

Modifier le code produit lors du Test technique pour que la validation mypy ne génère pas d’erreurs.