# Code propre A : de bons noms, de petites fonctions et une responsabilité unique<no value>

## 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 :

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

```python
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) :**

| Quoi | Style | Exemple |
|---|---|---|
| Variables et fonctions | `snake_case` | `total_price`, `send_email()` |
| Constantes (valeurs qui ne changent pas) | `UPPER_SNAKE_CASE` | `VAT_RATE`, `MAX_RETRIES` |
| Classes (le thème POO) | `PascalCase` | `Student`, `BankAccount` |
| Fichiers et modules | `snake_case.py` | `order_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.

{{< box "analogy" "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.
{{< /box >}}

## 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 :

```python
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` :

```python
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` :

```python
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) :**

```text
Average: 69.25 - pass
```

## Analyse du code ligne par ligne

| Dans l'ancien code | Dans le code propre | Pourquoi c'est mieux |
|---|---|---|
| `do` | Trois fonctions avec des noms | Le nom dit ce que fait chacune. |
| `g`, `s`, `x`, `a` | `grades`, `total`, `grade`, `average` | Pas besoin de « décoder ». |
| `55` au milieu du code | `PASSING_GRADE = 55` | Une valeur avec un sens et un nom. Si la note change, on la modifie à un seul endroit. |
| Calcul + décision + affichage dans une seule fonction | `calculate_average`, `is_passing`, `print_report` | Chaque 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 :**

```python
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 :**

```python
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 :**

```text
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.**

```python
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 :

```python
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()`.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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 :

```python
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 :

```text
550
60
```

{{< solution >}}
```python
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))
```

```text
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.
{{< /solution >}}

### Exercice 2: Les conventions de nommage

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

```text
TotalPrice      (a variable)
numberofstudents (a variable)
max_grade       (a variable)
vat_rate        (a constant that never changes)
GetAverage      (a function)
```

{{< solution >}}
| Nom | Correct ? | Nom correct |
|---|---|---|
| `TotalPrice` | Non — les variables s'écrivent en snake_case | `total_price` |
| `numberofstudents` | Non — difficile à lire sans tirets bas | `number_of_students` |
| `max_grade` | Oui | — |
| `vat_rate` | Non — les constantes s'écrivent en majuscules | `VAT_RATE` |
| `GetAverage` | Non — les fonctions s'écrivent en snake_case, et un verbe est préférable | `get_average` / `calculate_average` |
{{< /solution >}}

### 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 :

```python
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 :

```text
Dana: average 82.5 - passed
Avi: average 48.0 - failed
```

{{< solution >}}
```python
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)
```

```text
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.
{{< /solution >}}

### 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 :

```python
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 :**

```text
180
250
```

{{< solution >}}
```python
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))
```

```text
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.
{{< /solution >}}

### 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.

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

**Résultat attendu :**

```text
True
False
False
```

{{< solution >}}
```python
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"))
```

```text
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.
{{< /solution >}}

<!-- exercises:end -->

## 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.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [PEP 8 — Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions)
- [PEP 8 — Function and Variable Names](https://peps.python.org/pep-0008/#function-and-variable-names)
- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/)

<!-- docs:end -->
