Code propre A : de bons noms, de petites fonctions et une responsabilité unique

Objectifs de la leçon
  • Expliquer ce qu’est le Clean Code et pourquoi le code est lu bien plus souvent qu’il n’est écrit
  • Choisir des noms clairs pour les variables, les fonctions et les fichiers selon les conventions de Python
  • Reconnaître une fonction qui fait plus d’une chose et la découper
  • Appliquer le principe de responsabilité unique au niveau des fonctions

Qu’allons-nous apprendre ?

Jusqu’à présent, le but était « que le code fonctionne ». À partir d’aujourd’hui, le but est aussi qu’il soit clair. Nous allons apprendre :

  • Le nommage (Naming) — comment choisir des noms pour les variables, les fonctions et les fichiers.
  • Les petites fonctions (Small Functions) — pourquoi les fonctions doivent être courtes.
  • La responsabilité unique (Single Responsibility) — chaque fonction fait une seule chose.

Pourquoi en avons-nous besoin ?

Le code est lu bien plus souvent qu’il n’est écrit. Vous écrivez une fonction une fois, mais elle est lue à chaque correction de bug, à chaque nouvelle fonctionnalité et à chaque Code Review par un collègue. Et dans un mois, vous la lirez vous aussi comme un étranger.

Dans les entreprises, un code qui fonctionne mais qui n’est pas lisible est refusé en Code Review. Dans les entretiens d’embauche, on vérifie non seulement « avez-vous résolu le problème », mais aussi « la solution est-elle compréhensible ».

Comparez :

def f(l):
    t = 0
    for x in l:
        t = t + x[1] * x[2]
    return t * 1.17
VAT_RATE = 1.17

def calculate_order_total(items):
    subtotal = 0
    for item in items:
        subtotal = subtotal + item["price"] * item["quantity"]
    return subtotal * VAT_RATE

Les deux fonctions font exactement la même chose. La seconde se comprend sans effort.

Explication à partir des bases

Qu’est-ce que le Clean Code ?

Le Clean Code (code propre) est un code qu’une autre personne peut lire, comprendre et modifier facilement, et en toute confiance. Ce n’est pas un ensemble de règles rigides, mais un ensemble d’habitudes qui rendent le code clair.

Le nommage — de bons noms

Un bon nom répond à la question « qu’est-ce que c’est ? » ou « que fait-il ? » sans avoir à lire le code qui l’entoure.

Les conventions de nommage de Python (selon PEP 8, le guide de style officiel) :

QuoiStyleExemple
Variables et fonctionssnake_casetotal_price, send_email()
Constantes (valeurs qui ne changent pas)UPPER_SNAKE_CASEVAT_RATE, MAX_RETRIES
Classes (le thème POO)PascalCaseStudent, BankAccount
Fichiers et modulessnake_case.pyorder_utils.py

Les règles d’un bon nom :

  1. Décrit l’intention — days_until_exam vaut mieux que d.
  2. Fonction = verbe — calculate_total(), send_invoice(), is_valid_email(). Variable = nom — total, invoice, email.
  3. Un booléen sonne comme une question — is_active, has_permission, can_edit.
  4. Une liste au pluriel — students (une liste), student (un seul élément).
  5. Pas d’abréviations obscures — customer, pas cstmr. (Les abréviations connues comme id, url, max sont acceptables.)
  6. Pas de Magic Numbers — au lieu de * 1.17 dont personne ne connaît le sens, une constante nommée : VAT_RATE = 1.17.

Les petites fonctions

Une longue fonction (50 lignes et plus) est difficile à comprendre, difficile à tester et difficile à modifier. Règle empirique : si une fonction ne tient pas sur un écran, ou s’il est difficile de la nommer sans le mot « et », elle en fait probablement trop.

La responsabilité unique

Le principe de responsabilité unique (Single Responsibility Principle, SRP) : chaque fonction doit avoir une seule raison de changer, c’est-à-dire un seul rôle.

Analogie : un restaurant

Un restaurant a un serveur, un cuisinier et un caissier. Si le cuisinier prend aussi les commandes, cuisine et encaisse, chaque changement (un nouveau menu, un nouveau lecteur de cartes) oblige à le « changer », et chacune de ses erreurs nuit à tout. Quand chacun a un seul rôle, il est facile de remplacer, d’améliorer et de trouver où est le problème.

Syntaxe

Il n’y a pas de « nouvelle syntaxe » pour le code propre. C’est le même Python, simplement organisé autrement. Le modèle :

CONSTANT_NAME = value          # constants at the top, UPPER_SNAKE_CASE

def verb_noun(clear_parameter):   # a function does ONE thing, named by what it does
    ...
    return result

def main():                     # one function that connects the small pieces
    data = get_something()
    result = process_something(data)
    show_something(result)

main()

Premier exemple de code

Avant — un code qui fonctionne mais qui n’est pas propre. week-2/grades_before.py :

def do(g):
    s = 0
    for x in g:
        s = s + x
    a = s / len(g)
    if a >= 55:
        print("avg", a, "pass")
    else:
        print("avg", a, "fail")

do([80, 45, 92, 60])

Après — le même comportement, un code propre. week-2/grades_after.py :

PASSING_GRADE = 55

def calculate_average(grades):
    total = 0
    for grade in grades:
        total = total + grade
    return total / len(grades)

def is_passing(average):
    return average >= PASSING_GRADE

def print_report(average):
    if is_passing(average):
        status = "pass"
    else:
        status = "fail"
    print("Average:", average, "-", status)

student_grades = [80, 45, 92, 60]
average = calculate_average(student_grades)
print_report(average)

Sortie (la version « après ». L’ancienne version affiche avg 69.25 pass, le même calcul formulé de façon moins claire) :

Average: 69.25 - pass

Analyse du code ligne par ligne

Dans l’ancien codeDans le code proprePourquoi c’est mieux
doTrois fonctions avec des nomsLe nom dit ce que fait chacune.
g, s, x, agrades, total, grade, averagePas besoin de « décoder ».
55 au milieu du codePASSING_GRADE = 55Une valeur avec un sens et un nom. Si la note change, on la modifie à un seul endroit.
Calcul + décision + affichage dans une seule fonctioncalculate_average, is_passing, print_reportChaque fonction fait une seule chose.

L’avantage pratique : vous voulez afficher la moyenne sur un site web plutôt qu’à l’écran ? Seule print_report change. Vous voulez modifier la note de passage ? Seulement PASSING_GRADE. Et calculate_average est utile ailleurs aussi, car elle renvoie une valeur au lieu de l’afficher.

Un autre exemple

Une fonction « qui fait tout » dans un système d’inscription, et comment la découper. Avant :

def register(n, e, a):
    if "@" not in e:
        print("bad email")
        return
    if a < 16:
        print("too young")
        return
    u = n.strip().lower().replace(" ", ".")
    print("Welcome " + n + "! Your username is " + u)

Après :

MIN_AGE = 16

def is_valid_email(email):
    return "@" in email

def is_old_enough(age):
    return age >= MIN_AGE

def create_username(full_name):
    return full_name.strip().lower().replace(" ", ".")

def register_user(full_name, email, age):
    if not is_valid_email(email):
        print("Invalid email address")
        return
    if not is_old_enough(age):
        print("You must be at least", MIN_AGE, "to register")
        return
    username = create_username(full_name)
    print("Welcome " + full_name + "! Your username is " + username)

register_user("Dana Cohen", "dana@mail.com", 24)
register_user("Avi Levi", "avi-mail.com", 30)

Sortie :

Welcome Dana Cohen! Your username is dana.cohen
Invalid email address

Analyse :

  • "@" in email — in vérifie si un morceau de texte apparaît dans un autre texte, et renvoie True/False.
  • .strip() — supprime les espaces au début et à la fin. .lower() — met en minuscules. .replace(" ", ".") — remplace les espaces par des points. Ce sont des méthodes de str que l’on peut enchaîner l’une après l’autre.
  • return sans valeur — quitte immédiatement la fonction (renvoie None). Ainsi, on ne continue pas vers l’inscription après une erreur. Cette technique s’appelle Early Return (ou Guard Clause).
  • register_user « se lit maintenant comme une histoire » : vérifier l’e-mail, vérifier l’âge, créer un nom d’utilisateur, saluer. Tous les détails sont dans de petites fonctions.

Erreur courante

Erreur 1 : des noms qui mentent.

def get_user(user_id):
    user = find_in_list(user_id)
    user["last_login"] = "today"   # changes data!
    delete_old_sessions(user)      # deletes data!
    return user

Le nom dit « obtenir », mais la fonction modifie et supprime. Celui qui l’appelle pour « simplement obtenir un utilisateur » sera surpris. Un nom doit décrire tout ce que fait la fonction. Et s’il est difficile de la décrire sans « et », il y a plus d’une responsabilité.

Erreur 2 : le découpage excessif. L’inverse pose aussi problème :

def add_one(number):
    return number + 1

Une fonction d’une ligne qui cache une opération évidente ne fait qu’ajouter des sauts à la lecture. Découpez quand il y a une idée qui mérite un nom (is_old_enough), pas à chaque ligne.

Erreur 3 : des noms trop génériques. data, info, temp, result, manager, process(). Des noms comme ceux-là peuvent presque toujours être améliorés : data → student_records, process() → apply_discount().

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: Des noms qui s’expliquent d’eux-mêmes

Le code suivant fonctionne, mais il est impossible de comprendre ce qu’il fait :

def f(a, b):
    c = a * b
    if c > 500:
        c = c - 50
    return c

print(f(120, 5))
print(f(30, 2))

Réécrivez-le avec de bons noms pour la fonction et les variables, et remplacez les nombres 500 et 50 par des constantes nommées. La sortie doit rester la même :

550
60
Afficher la solution
DISCOUNT_THRESHOLD = 500
DISCOUNT_AMOUNT = 50


def calculate_order_price(unit_price, quantity):
    price = unit_price * quantity
    if price > DISCOUNT_THRESHOLD:
        price = price - DISCOUNT_AMOUNT
    return price


print(calculate_order_price(120, 5))
print(calculate_order_price(30, 2))
550
60
  • Le nom de la fonction est un verbe qui décrit ce qu’elle fait.
  • 500 et 50 étaient des Magic Numbers — maintenant on sait ce qu’ils sont, et si la remise change, on la modifie à un seul endroit.

Exercice 2: Les conventions de nommage

Pour chaque nom — est-il écrit selon les conventions de Python ? Sinon, écrivez le nom correct.

TotalPrice      (a variable)
numberofstudents (a variable)
max_grade       (a variable)
vat_rate        (a constant that never changes)
GetAverage      (a function)
Afficher la solution
NomCorrect ?Nom correct
TotalPriceNon — les variables s’écrivent en snake_casetotal_price
numberofstudentsNon — difficile à lire sans tirets basnumber_of_students
max_gradeOui—
vat_rateNon — les constantes s’écrivent en majusculesVAT_RATE
GetAverageNon — les fonctions s’écrivent en snake_case, et un verbe est préférableget_average / calculate_average

Exercice 3: Une fonction — une responsabilité

La fonction suivante calcule une moyenne, décide si l’étudiant a réussi, et affiche aussi — trois rôles :

def report(name, grades):
    total = 0
    for g in grades:
        total = total + g
    avg = total / len(grades)
    if avg >= 55:
        print(name + ": average", avg, "- passed")
    else:
        print(name + ": average", avg, "- failed")

report("Dana", [80, 85])
report("Avi", [50, 46])

Découpez-la en trois fonctions : calculate_average, is_passing et print_report(name, average). print_report reçoit une moyenne déjà calculée et ne fait qu’afficher. La sortie doit rester :

Dana: average 82.5 - passed
Avi: average 48.0 - failed
Afficher la solution
PASSING_GRADE = 55


def calculate_average(grades):
    return sum(grades) / len(grades)


def is_passing(average):
    return average >= PASSING_GRADE


def print_report(name, average):
    if is_passing(average):
        status = "passed"
    else:
        status = "failed"
    print(name + ": average", average, "-", status)


dana_average = calculate_average([80, 85])
print_report("Dana", dana_average)
avi_average = calculate_average([50, 46])
print_report("Avi", avi_average)
Dana: average 82.5 - passed
Avi: average 48.0 - failed
  • calculate_average ne fait que calculer, is_passing ne fait que décider, et print_report reçoit une moyenne prête et ne fait que l’afficher. Le calcul a lieu en dehors d’elle.
  • Si print_report appelait elle-même calculate_average, elle calculerait et afficherait de nouveau. Le code est plus court, mais les responsabilités ne sont pas séparées.
  • Maintenant, calculate_average peut aussi servir à un endroit qui n’a pas besoin d’afficher.

Exercice 4: Pas de nombres magiques

Dans une boutique en ligne, la fonction suivante reçoit le total de la commande et renvoie le montant à payer :

def final_price(total):
    if total < 200:
        total = total + 30
    return total

print(final_price(150))
print(final_price(250))
  1. Que signifient 200 et 30 dans la boutique ? (Indice : que se passe-t-il pour une petite commande ?) Donnez-leur des noms de constantes.
  2. Réécrivez la fonction pour que son nom explique exactement ce qu’elle ajoute.

Résultat attendu :

180
250
Afficher la solution
FREE_SHIPPING_MINIMUM = 200
SHIPPING_FEE = 30


def add_shipping(order_total):
    if order_total < FREE_SHIPPING_MINIMUM:
        return order_total + SHIPPING_FEE
    return order_total


print(add_shipping(150))
print(add_shipping(250))
180
250

200 est le montant minimum pour la livraison gratuite, et 30 est les frais de livraison. Avec des noms, personne n’a besoin de deviner.

Exercice 5: Une petite fonction qui répond à une seule question

Écrivez une fonction is_valid_password(password) qui renvoie True si le mot de passe fait au moins 8 caractères et contient au moins un chiffre, sinon False. La fonction n’affiche rien — elle se contente de renvoyer.

print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))

Résultat attendu :

True
False
False
Afficher la solution
MIN_PASSWORD_LENGTH = 8


def has_digit(text):
    for char in text:
        if char.isdigit():
            return True
    return False


def is_valid_password(password):
    return len(password) >= MIN_PASSWORD_LENGTH and has_digit(password)


print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))
True
False
False

Le nom is_valid_password commence par is_ — il est clair qu’il renvoie True ou False. La vérification du chiffre a été séparée dans une petite fonction à part.

Résumé

  • Clean Code = un code que les autres (et vous dans un mois) comprennent et modifient facilement. Le code est lu bien plus souvent qu’il n’est écrit.
  • Noms : snake_case pour les variables et les fonctions, UPPER_SNAKE_CASE pour les constantes. Fonction = verbe, booléen = question (is_...).
  • Pas de nombres magiques. Une valeur qui a un sens reçoit une constante nommée.
  • De petites fonctions avec une responsabilité unique (SRP). Si le nom a besoin de « et », il faut probablement découper.
  • Une fonction qui calcule renvoie. L’affichage est une responsabilité séparée.

Documentation officielle