# Les modules : pourquoi découper en fichiers, import, from et as<no value>

## Qu'allons-nous apprendre ?

- Ce qu'est un **module**.
- Pourquoi on découpe un projet en **plusieurs fichiers**.
- Trois formes d'import : `import`, `from ... import`, `as`.
- Ce qui se passe **exactement** quand Python importe un fichier.

## Pourquoi en avons-nous besoin ?

Nous avons déjà utilisé des modules : `import os`, `import logging`, `import socket`. Aujourd'hui, nous allons écrire **les nôtres**.

Un vrai projet fait des centaines et des milliers de lignes. Un seul fichier énorme est :

- **Difficile à parcourir** — où est la fonction qui envoie un e-mail ? Ligne 1 840 ?
- **Difficile pour le travail en équipe** — trois développeurs modifient le même fichier et se gênent (dans Git, dans le thème Git).
- **Difficile à réutiliser** — une bonne fonction de validation est « coincée » dans le fichier d'un jeu.

Découper en fichiers, c'est la **séparation des responsabilités au niveau du projet**. Dans les thèmes backend et bases de données, le serveur sera découpé exactement ainsi : `main.py` (les routes), `db.py` (l'accès à la base de données), `models.py`.

## Explication à partir des bases

### Qu'est-ce qu'un module ?

**Module = n'importe quel fichier `.py`.** C'est tout. Chaque fichier Python que vous avez écrit est déjà un module, et vous pouvez en importer des fonctions, des constantes et des variables dans un autre fichier.

- Le nom du module = le nom du fichier **sans** `.py`. Le fichier `prices.py` → le module `prices`.
- C'est pourquoi les noms de fichiers doivent être des noms Python valides : `snake_case`, sans tirets (`my-utils.py` ne s'importera pas !), et sans commencer par un chiffre.

{{< box "analogy" "Analogie : une cuisine avec des tiroirs" >}}
Un seul fichier énorme = un seul tiroir avec tous les ustensiles : couteaux, cuillères, pinces, ciseaux. Pour trouver quelque chose, on fouille dans tout. **Les modules = des tiroirs séparés** avec des étiquettes : « couverts », « ustensiles de pâtisserie », « épices ». `import` = « apporte-moi quelque chose de ce tiroir ».
{{< /box >}}

### Trois formes d'import

| Forme | Comment on l'utilise ensuite | Quand |
|---|---|---|
| `import prices` | `prices.add_vat(100)` | Par défaut. On sait clairement d'où vient chaque nom. |
| `from prices import add_vat` | `add_vat(100)` | Quand on utilise souvent quelques noms précis. |
| `import prices as p` / `from prices import add_vat as vat` | `p.add_vat(100)` / `vat(100)` | Un nom long ou une collision de noms. Conventions connues : `import numpy as np`. |

### Que se passe-t-il au moment de l'`import` ?

1. Python **cherche** le fichier : d'abord dans le dossier du script que vous avez lancé, puis dans les bibliothèques de Python.
2. Python **exécute tout le fichier de haut en bas**, une fois : il crée les fonctions et les constantes, et **exécute tout code situé au niveau supérieur**.
3. Le résultat est conservé. Un autre `import` du même module **ne l'exécute pas à nouveau**.

Le point 2 est très important, et nous verrons ses conséquences dans l'erreur courante.

### `__pycache__`

Après le premier import, un dossier nommé `__pycache__` avec des fichiers `.pyc` apparaîtra. C'est le **bytecode** (souvenez-vous du thème sur l'ordinateur et le système d'exploitation !) des modules, enregistré pour que l'import suivant soit plus rapide. N'y touchez pas, et dans les projets Git, ignorez-le (nous le verrons dans `.gitignore` dans le thème Git).

## Syntaxe

```python
# file: prices.py   (the module)
VAT_RATE = 0.17

def add_vat(amount):
    return amount * (1 + VAT_RATE)
```

```python
# file: main.py     (uses the module; same folder)
import prices
prices.add_vat(100)

from prices import add_vat, VAT_RATE
add_vat(100)

import prices as p
p.add_vat(100)

from prices import add_vat as with_vat
with_vat(100)
```

**L'ordre en usage en haut d'un fichier :** d'abord les imports de la bibliothèque standard (`os`, `json`), une ligne vide, puis les imports de vos propres modules.

## Premier exemple de code

Créez un dossier `week-2/shop/`, avec deux fichiers dedans.

`week-2/shop/prices.py` :

```python
VAT_RATE = 0.17

def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)

def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

`week-2/shop/main.py` :

```python
import prices

base_price = 200
discounted = prices.apply_discount(base_price, 10)
final_price = prices.add_vat(discounted)

print("Base:", base_price)
print("After 10% discount:", discounted)
print("With VAT (" + str(prices.VAT_RATE) + "):", final_price)
```

**Exécution** (depuis `week-2/shop`) : `python main.py`

**Sortie :**

```text
Base: 200
After 10% discount: 180.0
With VAT (0.17): 210.6
```

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `import prices` | Python cherche `prices.py` **dans le même dossier** que `main.py`, l'exécute, et crée le nom `prices` qui représente le module. |
| `prices.apply_discount(...)` | On accède à la fonction **par** le nom du module, avec un point. Exactement comme `os.getcwd()`. |
| `prices.VAT_RATE` | Les constantes sont accessibles de la même façon. |
| `main.py` ne définit aucune fonction de prix | Toute la logique des prix est à **un seul endroit**. Si la TVA change, on ne modifie que `prices.py`. |

## Un autre exemple

**Les trois formes d'import, et un second module.** Ajoutez `week-2/shop/validation.py` :

```python
MIN_NAME_LENGTH = 2

def is_valid_price(price):
    return isinstance(price, (int, float)) and price >= 0

def is_valid_name(name):
    return len(name.strip()) >= MIN_NAME_LENGTH
```

Et mettez à jour `main.py` :

```python
import prices as p
from validation import is_valid_price, is_valid_name

products = [("Laptop", 3500), ("X", 20), ("Mouse", -5), ("Desk", 900)]

for name, price in products:
    if not is_valid_name(name):
        print("Skipping invalid name:", repr(name))
    elif not is_valid_price(price):
        print("Skipping invalid price for", name, ":", price)
    else:
        print(name, "->", p.add_vat(price), "NIS incl. VAT")
```

**Sortie :**

```text
Laptop -> 4095.0 NIS incl. VAT
Skipping invalid name: 'X'
Skipping invalid price for Mouse : -5
Desk -> 1053.0 NIS incl. VAT
```

**Analyse :**

- `import prices as p` — un surnom court. On y accède avec `p.add_vat`.
- `from validation import is_valid_price, is_valid_name` — importe **seulement** les deux fonctions, et les utilise directement, sans préfixe.
- `repr(name)` — affiche la valeur avec des guillemets (`'X'`), comme `%r` dans les journaux.
- **La structure du projet :** `prices.py` = la logique des prix. `validation.py` = les règles de validité. `main.py` = relie et exécute. Chaque fichier a une seule responsabilité.

## Erreur courante

**Erreur 1 : du code de « test » au niveau supérieur d'un module.** Supposons qu'en bas de `prices.py` vous ayez ajouté un test :

```python
print("Testing prices module:", add_vat(100))
```

Désormais, **chaque** `import prices`, depuis n'importe quel fichier, affichera cette ligne, car l'import **exécute le fichier**. Dans la leçon suivante, nous apprendrons la solution : `if __name__ == "__main__":`.

**Erreur 2 : un nom de fichier qui entre en collision avec un module standard.**

Vous avez créé un fichier nommé `random.py` (ou `logging.py`, `json.py`), et dans un autre fichier vous avez écrit `import random` :

```text
AttributeError: module 'random' has no attribute 'randint'
```

Python trouve **votre** `random.py` **en premier**, dans le même dossier, et l'importe à la place du vrai module. Ne nommez jamais des fichiers avec les noms de modules standards.

**Erreur 3 : `ModuleNotFoundError`.**

```text
ModuleNotFoundError: No module named 'prices'
```

Les raisons courantes : le fichier n'est pas dans le même dossier que le script lancé, une faute de frappe dans le nom, ou un nom de fichier avec un tiret (`my-prices.py`).

**Erreur 4 : `from module import *`.** Importe **tous** les noms du module. On ne sait pas d'où vient chaque nom, et des noms peuvent s'écraser silencieusement. Importez explicitement ce dont vous avez besoin.

<!-- 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: Le premier module

Créez un fichier `prices.py` avec la constante `VAT_RATE = 0.17` et deux fonctions : `add_vat(amount)` et `apply_discount(amount, percent)` (toutes deux arrondissent à deux chiffres). Dans le fichier `main.py`, importez le module, calculez un prix de 200 après une remise de 10 %, puis ajoutez-lui la TVA.

**Résultat attendu (en exécutant main.py) :**

```text
After discount: 180.0
With VAT: 210.6
```

{{< solution >}}
**prices.py**

```py
VAT_RATE = 0.17


def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)


def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

**main.py**

```py
import prices

discounted = prices.apply_discount(200, 10)
final_price = prices.add_vat(discounted)
print("After discount:", discounted)
print("With VAT:", final_price)
```

`import prices` charge le fichier `prices.py` qui se trouve dans le même dossier. On accède à son contenu avec un point : `prices.add_vat(...)`.
{{< /solution >}}

### Exercice 2: Trois formes d'import

Écrivez la même ligne — calculer la TVA sur 100 — dans les trois formes d'import : `import`, `from ... import` et `import ... as`. Quand vaut-il la peine d'utiliser chacune ?

{{< solution >}}
```py
import prices
print(prices.add_vat(100))

from prices import add_vat
print(add_vat(100))

import prices as p
print(p.add_vat(100))
```

- **`import prices`** — on sait toujours clairement d'où vient chaque nom (`prices.add_vat`). Convient quand on utilise beaucoup de choses du module.
- **`from prices import add_vat`** — on écrit moins. Convient à quelques noms précis que l'on utilise beaucoup.
- **`import prices as p`** — un nom court pour un module au nom long.
{{< /solution >}}

### Exercice 3: La bibliothèque standard

Sans écrire votre propre module, utilisez les modules intégrés de Python :

1. `math` — la racine carrée de 81, et `math.pi` arrondi à deux chiffres.
2. `random` — avec `random.seed(1)`, tirez un nombre entre 1 et 6.

**Résultat attendu :**

```text
9.0
3.14
2
```

{{< solution >}}
```python
import math
import random

print(math.sqrt(81))
print(round(math.pi, 2))

random.seed(1)
print(random.randint(1, 6))
```

```text
9.0
3.14
2
```

`math` et `random` font partie de la **bibliothèque standard** (Standard Library) — ils sont fournis avec Python, et on les importe exactement comme un module que nous avons écrit nous-mêmes. `random.seed(1)` fixe le tirage, pour que le résultat soit le même à chaque exécution.
{{< /solution >}}

### Exercice 4: Du code qui s'exécute à l'import

Dans le fichier `helpers.py` :

```text
def shout(text):
    return text.upper() + "!"

print("helpers loaded")
```

Dans le fichier `main.py` :

```text
from helpers import shout
print(shout("hi"))
```

1. Qu'est-ce qui sera affiché quand vous lancerez `python main.py`, et pourquoi ?
2. Comment empêcher la première ligne ?

{{< solution >}}
1. Ceci sera affiché :

```text
helpers loaded
HI!
```

**`import` exécute tout le fichier** — y compris un `print` situé au niveau supérieur. Même `from helpers import shout` exécute tout `helpers.py`, et pas seulement la fonction.

2. Un module destiné à être importé ne doit pas avoir de code au niveau supérieur — seulement des définitions. Supprimez le `print`. (Dans la leçon suivante, nous apprendrons `if __name__ == "__main__":`, qui permet de garder du code de test qui ne s'exécute que lorsque le fichier est lancé directement.)
{{< /solution >}}

### Exercice 5: Un module de températures

Créez un module `temperature.py` avec `c_to_f(celsius)` et avec `describe(celsius)` qui renvoie `"hot"` à partir de 30 degrés, `"cold"` en dessous de 15, et `"pleasant"` entre les deux. Dans `main.py`, affichez pour chaque ville la température dans les deux unités et la description.

```py
cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]
```

**Résultat attendu :**

```text
Eilat: 35 C / 95.0 F - hot
Haifa: 22 C / 71.6 F - pleasant
Jerusalem: 9 C / 48.2 F - cold
```

{{< solution >}}
**temperature.py**

```py
HOT_FROM = 30
COLD_BELOW = 15


def c_to_f(celsius):
    return round(celsius * 9 / 5 + 32, 1)


def describe(celsius):
    if celsius >= HOT_FROM:
        return "hot"
    if celsius < COLD_BELOW:
        return "cold"
    return "pleasant"
```

**main.py**

```py
from temperature import c_to_f, describe

cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]

for i in range(len(cities)):
    celsius = temperatures[i]
    print(cities[i] + ":", celsius, "C /", c_to_f(celsius), "F -", describe(celsius))
```

La logique des températures est dans le module, et `main.py` ne fait que l'utiliser et afficher.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Module = un fichier `.py`.** Le nom du module = le nom du fichier sans `.py` (`snake_case`, sans tirets).
- `import m` → `m.func()`. `from m import func` → `func()`. `import m as x` → `x.func()`.
- Python cherche les modules d'abord **dans le dossier du script**. Ne nommez pas de fichiers `random.py`, `json.py`, etc.
- L'import **exécute le module** une fois. Le code au niveau supérieur s'exécute même lors d'un import.
- Découper en fichiers = la séparation des responsabilités au niveau du projet. Pas d'`import *`.

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

## Documentation officielle

- [Python Tutorial — Modules](https://docs.python.org/3/tutorial/modules.html)
- [More on Modules](https://docs.python.org/3/tutorial/modules.html#more-on-modules)

<!-- docs:end -->
