Chapitre 415 minDébutant
Fonctions, modules et import en Python
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- définir une fonction qui reçoit des paramètres et renvoie un résultat ;
- utiliser les paramètres par défaut et les arguments nommés ;
- éviter les pièges de la portée et de la valeur par défaut modifiable ;
- découper un script en modules et les importer.
3 exercices corrigés
Une fonction est un bloc de code nommé, qui reçoit des paramètres et renvoie un résultat ; un module est un fichier .py qui regroupe des fonctions réutilisables, qu'on charge avec import. Ce chapitre montre comment définir une fonction avec def, utiliser les paramètres par défaut et les arguments nommés, documenter et annoter une fonction, éviter deux pièges classiques, puis découper un script en modules avec le fameux if __name__ == "__main__".
Définir et appeler une fonction
# fonctions.py : définir et appeler des fonctions
def taux_occupation(utilise: float, total: float) -> float:
"""Renvoie le taux d'occupation en pourcentage."""
return utilise / total * 100
def niveau(taux: float, alerte: float = 80, critique: float = 90) -> str:
"""Renvoie OK, ALERTE ou CRITIQUE selon les deux seuils."""
if taux >= critique:
return "CRITIQUE"
if taux >= alerte:
return "ALERTE"
return "OK"
def extremes(valeurs: list[float]) -> tuple[float, float, float]:
"""Renvoie le minimum, le maximum et la moyenne d'une liste non vide."""
return min(valeurs), max(valeurs), sum(valeurs) / len(valeurs)
t = taux_occupation(437.5, 500)
print(t, niveau(t))
print(niveau(t, critique=85)) # argument nommé : alerte garde 80
print(niveau(t, alerte=90, critique=95))
mini, maxi, moyenne = extremes([61.0, 87.5, 93.2, 44.8])
print(f"min {mini}, max {maxi}, moyenne {moyenne:.1f}")
87.5 ALERTE
CRITIQUE
OK
min 44.8, max 93.2, moyenne 71.6
defintroduit la fonction, suivie de son nom, de ses paramètres entre parenthèses et des deux-points. Le corps est indenté.returnrenvoie une valeur et termine la fonction. Une fonction peut avoir plusieursreturn, commeniveau(): le premier atteint gagne.- Paramètres par défaut.
alerte: float = 80rend le paramètre facultatif : sans valeur fournie, il vaut 80. - Arguments nommés. À l'appel,
niveau(t, critique=85)fixe un paramètre par son nom et laisse les autres à leur valeur par défaut. C'est plus lisible queniveau(t, 80, 85): on voit ce que vaut chaque nombre. - Plusieurs résultats.
extremes()renvoie trois valeurs séparées par des virgules (un tuple, chapitre 5) ; l'appel les range dans trois variables d'un coup.
Le texte entre triples guillemets sous le def est la docstring : la documentation de la fonction, que help(niveau) affiche. Les annotations de type (taux: float, -> str) disent ce que la fonction attend et renvoie. Python ne les vérifie pas à l'exécution, mais ton éditeur s'en sert pour te signaler une erreur avant même de lancer le script. list[float] se lit « liste de flottants ».
Portée des variables
Une variable affectée dans une fonction est locale : elle naît à l'appel et disparaît au retour, sans toucher à une variable du même nom à l'extérieur. Une fonction peut lire une variable globale, mais dès qu'elle l'affecte, Python la considère comme locale dans toute la fonction :
# portee.py : une fonction qui modifie une variable globale
total = 0
def ajouter(go):
total = total + go # total est locale ici : elle n'a pas encore de valeur
return total
ajouter(500)
Traceback (most recent call last):
File "/home/etudiant/cours-python/portee.py", line 10, in <module>
ajouter(500)
~~~~~~~^^^^^
File "/home/etudiant/cours-python/portee.py", line 6, in ajouter
total = total + go # total est locale ici : elle n'a pas encore de valeur
^^^^^
UnboundLocalError: cannot access local variable 'total' where it is not associated with a value
Le traceback se lit de bas en haut : la dernière ligne donne l'erreur, celles du dessus montrent le chemin, de l'appel à la ligne 10 jusqu'à la ligne 6 dans ajouter (lire un traceback). La correction n'est pas le mot-clé global, qui existe mais rend le code difficile à suivre : passe la valeur en paramètre et renvoie le résultat, total = ajouter(total, 500).
Le piège de la valeur par défaut modifiable
# defaut_mutable.py : le piège de la liste par défaut
def ajouter_poste(poste, inventaire=[]):
inventaire.append(poste)
return inventaire
print(ajouter_poste("PC-001"))
print(ajouter_poste("PC-002")) # on attend ['PC-002']...
def ajouter_poste_corrige(poste, inventaire=None):
if inventaire is None:
inventaire = [] # une nouvelle liste à chaque appel
inventaire.append(poste)
return inventaire
print(ajouter_poste_corrige("PC-001"))
print(ajouter_poste_corrige("PC-002"))
['PC-001']
['PC-001', 'PC-002']
['PC-001']
['PC-002']
La valeur par défaut est créée une seule fois, quand Python lit le def, et pas à chaque appel. La liste [] est donc partagée par tous les appels qui n'en fournissent pas : le deuxième appel retrouve PC-001. La règle : jamais de liste ni de dictionnaire comme valeur par défaut. Utilise None, et crée la liste dans la fonction.
Importer un module
Un module est un fichier .py ; un paquet, un dossier de modules. Trois façons d'importer :
# imports.py : trois façons d'importer
import math
from datetime import date
import statistics as stats
print(math.sqrt(144), math.ceil(437.5 / 100), math.gcd(84, 36))
print(date(2026, 11, 8).isoformat())
print(stats.median([61.0, 87.5, 93.2, 44.8]))
12.0 5 12
2026-11-08
74.25
| Forme | Usage | Appel |
|---|---|---|
import math | Importe le module entier | math.sqrt(144) : on voit d'où vient la fonction |
from datetime import date | Importe un nom précis | date(2026, 11, 8) |
import statistics as stats | Importe sous un alias | stats.median(...) |
Évite from math import * : il déverse tous les noms du module dans ton script, et tu ne sais plus d'où vient une fonction. Les imports se placent en tête de fichier : d'abord la bibliothèque standard, puis les paquets installés, puis tes propres modules.
Dans la console, dir(math) liste le contenu d'un module et help(math.gcd) explique une fonction : c'est l'aide syntaxique que tu peux consulter pendant l'épreuve E3.
Écrire ton propre module
Dès qu'un script dépasse une centaine de lignes, ou que deux scripts ont besoin des mêmes fonctions, range ces fonctions dans un module. Voici outils_disque.py :
"""outils_disque.py : fonctions réutilisables sur l'occupation des disques."""
SEUIL_ALERTE = 80
SEUIL_CRITIQUE = 90
def taux_occupation(utilise: float, total: float) -> float:
"""Renvoie le taux d'occupation en pourcentage."""
return utilise / total * 100
def niveau(taux: float) -> str:
"""Renvoie OK, ALERTE ou CRITIQUE selon les seuils du module."""
if taux >= SEUIL_CRITIQUE:
return "CRITIQUE"
if taux >= SEUIL_ALERTE:
return "ALERTE"
return "OK"
if __name__ == "__main__":
# Exécuté seulement quand on lance ce fichier, pas quand on l'importe
assert taux_occupation(50, 200) == 25
assert niveau(79.9) == "OK"
assert niveau(80) == "ALERTE"
assert niveau(95) == "CRITIQUE"
print("outils_disque : tests OK")
Et un script qui s'en sert, dans le même dossier :
# rapport.py : utilise le module outils_disque, rangé dans le même dossier
from outils_disque import niveau, taux_occupation
volumes = [("srv-ad", 38, 120), ("srv-fichiers", 437.5, 500), ("srv-journaux", 186, 200)]
for nom, utilise, total in volumes:
taux = taux_occupation(utilise, total)
print(f"{nom:<14}{taux:6.1f} % {niveau(taux)}")
srv-ad 31.7 % OK
srv-fichiers 87.5 % ALERTE
srv-journaux 93.0 % CRITIQUE
Lancé directement, le module exécute ses tests :
outils_disque : tests OK
if name == "main"
Quand Python exécute un fichier, il donne à la variable spéciale __name__ la valeur "__main__" pour le fichier lancé, et le nom du module ("outils_disque") pour un fichier importé. Le bloc if __name__ == "__main__": ne s'exécute donc que si tu lances le fichier lui-même. Tu y mets le code de démarrage d'un script ou, comme ici, des vérifications rapides avec assert. Sans ce test, le simple import outils_disque de rapport.py aurait lancé les tests et affiché leur message.
assert condition ne fait rien si la condition est vraie, et arrête le programme avec une AssertionError sinon. C'est le moyen le plus simple de vérifier une fonction ; le chapitre 15 passe aux vrais tests avec pytest.
Exercices
Exercice 1 sur 3 Pourquoi None ? Réussi À revoir
Ce programme affiche None puis plante. Explique pourquoi et corrige-le.
def hotes(prefixe):
"""Nombre d'adresses d'hôtes d'un réseau IPv4 de ce préfixe."""
nombre = 2 ** (32 - prefixe) - 2
resultat = hotes(26)
print("Un /26 contient", resultat, "hôtes")
print(resultat + 1)
Voir la réponseMasquer la réponse de l'exercice 1
Un /26 contient None hôtes
Traceback (most recent call last):
File "/home/etudiant/cours-python/ex_none.py", line 8, in <module>
print(resultat + 1)
~~~~~~~~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
La fonction calcule nombre mais ne le renvoie pas : une fonction sans return renvoie None. print affiche donc None, et None + 1 provoque une TypeError. La correction : remplacer la ligne de calcul par return 2 ** (32 - prefixe) - 2. L'appel hotes(26) renvoie alors 62. Confondre « afficher » (print) et « renvoyer » (return) est l'erreur la plus fréquente des débutants : une fonction de calcul renvoie, c'est l'appelant qui décide d'afficher.
Tu avais trouvé ?
Exercice 2 sur 3 Arguments nommés Réussi À revoir
Avec la fonction niveau(taux, alerte=80, critique=90) de ce chapitre, que renvoient ces appels ?
niveau(85)niveau(85, 90)niveau(85, critique=85)niveau(alerte=70, taux=75)niveau(95, alerte=99)
Voir la réponseMasquer la réponse de l'exercice 2
"ALERTE": 85 ≥ 80, mais 85 < 90."OK": 90 est passé en deuxième position, donc àalerte. Les deux seuils valent alors 90, et 85 n'atteint ni l'un ni l'autre."CRITIQUE": 85 ≥ 85."ALERTE": tous les arguments sont nommés, l'ordre ne compte plus ; 75 ≥ 70 et 75 < 90."CRITIQUE": 95 ≥ 90 est testé en premier. Le seuil d'alerte à 99, plus haut que le seuil critique, n'a aucun sens ; une fonction robuste le refuserait avecraise ValueError(...)(chapitre 8).
Tu avais trouvé ?
Exercice 3 sur 3 Un module de conversion Réussi À revoir
Écris un module tailles.py avec une fonction formater_taille(octets) qui renvoie la taille avec l'unité binaire adaptée, avec un chiffre après la virgule : 512 donne "512 o", 1536 donne "1.5 Kio", 5 × 1024³ donne "5.0 Gio". Les unités vont jusqu'au Tio. Ajoute des tests avec assert sous if __name__ == "__main__":.
Voir la réponseMasquer la réponse de l'exercice 3
"""tailles.py : afficher une taille en octets avec la bonne unité."""
UNITES = ["o", "Kio", "Mio", "Gio", "Tio"]
def formater_taille(octets: int) -> str:
"""Renvoie la taille avec l'unité binaire adaptée : 1536 -> '1.5 Kio'."""
taille = octets
rang = 0
while taille >= 1024 and rang < len(UNITES) - 1:
taille = taille / 1024
rang = rang + 1
if rang == 0:
return f"{octets} o"
return f"{taille:.1f} {UNITES[rang]}"
if __name__ == "__main__":
assert formater_taille(512) == "512 o"
assert formater_taille(1536) == "1.5 Kio"
assert formater_taille(5 * 1024 ** 3) == "5.0 Gio"
assert formater_taille(3 * 1024 ** 5) == "3072.0 Tio"
print(formater_taille(437_500_000_000))
print("tests OK")
407.5 Gio
tests OK
On divise par 1024 tant que la taille atteint 1024 et qu'il reste une unité plus grande. La condition rang < len(UNITES) - 1 empêche de sortir de la liste : au-delà du Tio, on reste en Tio (3072.0 Tio pour 3 Pio). 437,5 milliards d'octets font 407,5 Gio : la différence avec les « 437,5 Go » du fabricant vient de là, 1 Go = 10⁹ octets alors que 1 Gio = 2³⁰ octets.
Tu avais trouvé ?