Aller au contenu
English

Le contrat d'une fonction

  • Leçon 7
  • 40 min
  • Module 3 : Fonctions et structure du programme
Leçon 7 sur 18 · Python pour l'ingénierie de l'IA en pratiqueLe contrat d'une fonction

Ce que vous saurez faire

Divisez count_tickets.py en petites fonctions, chacune avec une seule tâche et un contrat clair. Ce contrat précise les entrées, les sorties et ce que la fonction ne change pas. Vous pourrez vérifier chaque partie séparément et la réutiliser dans d’autres programmes.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter contient tous les exemples, tâches et vérifications de cette leçon, avec les instructions. Il contient aussi le même code de count_tickets.py. Il ne demande aucune installation ni projet local : seulement Python. Lisez les explications ici et exécutez le code là-bas.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 40 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur (seulement pour le parcours local; le carnet et les encadrés de cette page n’en ont pas besoin)

Testé avec Python 3.14 (terminal) et Pyodide 314.0 (navigateur). Le carnet a été testé dans Jupyter.

À la fin du module 2, count_tickets.py fait tout du début à la fin : filtrer, compter, calculer la moyenne et afficher. Il fonctionne, mais trois choses sont difficiles :

  • Pour vérifier une partie, vous devez tout exécuter et lire la sortie affichée.
  • Pour réutiliser une partie, par exemple le comptage dans un autre rapport, vous devez copier les lignes.
  • Pour le lire, vous devez suivre chaque variable depuis le début, car n’importe quelle ligne peut changer n’importe quelle variable.

Une fonction est un bloc de code nommé que vous pouvez exécuter plusieurs fois. Elle reçoit des valeurs d’entrée, fait une seule tâche et renvoie un résultat. Vous utilisez déjà des fonctions fournies par Python : len(), print() et round(). Maintenant, vous écrivez les vôtres.

def count_by_category(tickets):
"""Return the number of tickets in each category."""
counts = {}
for ticket in tickets:
category = ticket["category"]
counts[category] = counts.get(category, 0) + 1
return counts
Partie Signification
def Commence la définition d’une fonction. Python garde la fonction en mémoire. Il n’exécute pas encore son corps.
count_by_category Le nom. Utilisez un verbe qui indique ce que fait la fonction.
(tickets) Le paramètre : un nom pour la valeur fournie par le code appelant.
"""Return ...""" La chaîne de documentation (docstring) : une phrase qui indique ce que la fonction renvoie.
Le corps indenté Le travail. Il s’exécute chaque fois qu’on appelle la fonction.
return counts Termine la fonction et renvoie la valeur au code appelant.

Pour appeler la fonction, écrivez son nom avec une valeur entre parenthèses. Cette valeur est l’argument. Python donne la valeur de l’argument au paramètre tickets, exécute le corps et remplace l’appel par la valeur renvoyée :

category_counts = count_by_category(tickets)

Le contrat d’une fonction est sa promesse au code qui l’appelle. Écrivez-le en mots simples avant d’écrire le corps :

Question count_by_category
Quelles sont les entrées? Une liste de dictionnaires de billets. Chacun possède une clé "category" (catégorie).
Quelles sont les sorties? Un dictionnaire : catégorie → nombre de billets. Une entrée vide donne {}.
Que ne fait pas la fonction? Elle ne change pas la liste et n’affiche rien.

La dernière ligne est la plus importante. Une fonction qui lit seulement ses entrées et renvoie un résultat est une fonction pure. Pour les mêmes entrées, elle donne toujours la même sortie et n’a aucun autre effet. Les fonctions pures sont les plus faciles à vérifier : fournissez une entrée, puis comparez la sortie. Gardez les parties qui produisent des effets, comme print ou l’écriture d’un fichier, à quelques endroits en périphérie du programme.

Les débutants écrivent souvent une fonction qui affiche son résultat. Cette fonction peut montrer un résultat, mais aucun autre code ne peut l’utiliser :

def show_count(tickets):
print(len(tickets))
result = show_count(tickets) # prints 6
print(result) # prints None

Une fonction sans instruction return renvoie None (aucune valeur). Donc, result vaut None, et vous ne pouvez pas l’utiliser dans un calcul. Renvoyez la valeur et laissez le code appelant décider quoi en faire : l’afficher, l’enregistrer ou la vérifier.

Les variables du corps d’une fonction, comme counts et category, sont locales : elles existent seulement pendant son exécution. Après l’appel, elles n’existent plus. Hors de la fonction, print(counts) s’arrête avec NameError: name 'counts' is not defined (le nom « counts » n’est pas défini).

C’est une protection, pas une limite. Une fonction ne peut pas changer par accident une variable du code appelant. Deux fonctions peuvent utiliser le nom counts sans s’affecter. La seule façon de faire sortir une valeur est return.

Exécutez du Python dans votre navigateur

Appelez une fonction et utilisez le résultat

La même fonction s’exécute trois fois avec des arguments différents. Ajoutez une ligne à la fin : print(counts). Prédisez ce qui se passera, puis exécutez le code.

Un paramètre peut avoir une valeur par défaut. Le code appelant peut omettre cet argument, et la fonction utilise alors la valeur par défaut :

def filter_by_status(tickets, status="open"):
"""Return the tickets that have the given status."""
selected = []
for ticket in tickets:
if ticket["status"] == status:
selected.append(ticket)
return selected
filter_by_status(tickets) # the open tickets
filter_by_status(tickets, "closed") # the closed tickets
filter_by_status(tickets, status="closed")

Le dernier appel nomme l’argument : status="closed". Les arguments nommés rendent un appel plus facile à lire, surtout quand une fonction a plus de deux paramètres. Les paramètres avec une valeur par défaut viennent après ceux qui n’en ont pas.

Exemple détaillé : count_tickets.py sous forme de fonctions

Section intitulée « Exemple détaillé : count_tickets.py sous forme de fonctions »

Voici le script du module 2, réécrit sous forme de fonctions. Chaque fonction a une seule tâche. main() les appelle dans l’ordre et c’est la seule fonction qui affiche quelque chose.

count_tickets.py
TICKETS = [
{"id": "T-1001", "status": "open", "category": "billing", "priority": 2},
{"id": "T-1002", "status": "closed", "category": "login", "priority": 3},
{"id": "T-1003", "status": "open", "category": "login", "priority": 1},
{"id": "T-1004", "status": "pending", "category": "billing", "priority": 2},
{"id": "T-1005", "status": "open", "category": "billing", "priority": 3},
{"id": "T-1006", "status": "closed", "category": "shipping", "priority": 1},
]
def filter_by_status(tickets, status="open"):
"""Return the tickets that have the given status."""
selected = []
for ticket in tickets:
if ticket["status"] == status:
selected.append(ticket)
return selected
def count_by_category(tickets):
"""Return the number of tickets in each category, sorted by category."""
counts = {}
for ticket in tickets:
category = ticket["category"]
counts[category] = counts.get(category, 0) + 1
return dict(sorted(counts.items()))
def average_priority(tickets):
"""Return the average priority, or None when there are no tickets."""
if not tickets:
return None
total = 0
for ticket in tickets:
total = total + ticket["priority"]
return round(total / len(tickets), 1)
def main():
status = "open"
selected = filter_by_status(TICKETS, status)
print(f"Tickets with status '{status}': {len(selected)}")
if not selected:
print("No tickets to count.")
return
for category, count in count_by_category(selected).items():
print(f" {category}: {count}")
print(f"Average priority: {average_priority(selected)}")
main()

Trois détails :

  • TICKETS est en majuscules. En Python, cela signifie « une constante : cette valeur ne change pas pendant l’exécution du programme ».
  • average_priority renvoie None pour une entrée vide, au lieu de s’arrêter avec ZeroDivisionError (erreur de division par zéro). La chaîne de documentation le précise, donc le code appelant sait qu’il doit vérifier.
  • return sans valeur dans main() termine la fonction plus tôt. Ici, il permet de sortir en cas d’entrée vide, comme break dans une boucle.

La dernière ligne, main(), appelle main. Sans elle, le script définit quatre fonctions, puis se termine sans rien faire.

Remanier signifie « changer la structure du code sans changer ce qu’il fait ». La sortie doit rester la même avant et après.

  1. Exécutez votre count_tickets.py actuel. Copiez sa sortie dans un commentaire à la fin du fichier ou dans un fichier texte. Cette sortie servira à la comparaison.

  2. Remplacez le contenu du fichier par l’exemple détaillé. Gardez vos propres données de billets si vous les avez modifiées.

  3. Exécutez-le : python count_tickets.py.

    Sortie
    Tickets with status 'open': 3
    billing: 2
    login: 1
    Average priority: 2.0
  4. Comparez la sortie avec celle de l’étape 1. Elle doit être identique. Si elle diffère, trouvez quelle fonction donne une valeur différente. Affichez son résultat dans main().

  5. Dans main(), changez status pour "resolved" (résolu), puis exécutez le script de nouveau. La sortie est le nombre 0 et No tickets to count. (aucun billet à compter).

Chaque fonction est pure, donc quelques lignes suffisent pour la vérifier. Appelez-la avec une petite entrée dont vous pouvez calculer le résultat de tête, puis comparez le résultat. assert arrête le programme avec une erreur si une condition vaut False (faux). Il ne fait rien si elle vaut True (vrai).

Exécutez du Python dans votre navigateur

Vérifiez les fonctions

Chaque assert vérifie un petit fait du contrat. Ajoutez la vérification manquante à la fin. Puis, brisez volontairement la fonction : remplacez return None par return 0 et exécutez-la de nouveau. Quelle ligne échoue?

Dans le module 5, vous déplacez ces vérifications dans des fichiers de test. Un outil nommé pytest les exécute pour vous.

Ajoutez une fonction urgent_ids(tickets) à count_tickets.py. Voici son contrat :

Question Réponse
Quelles sont les entrées? Une liste de dictionnaires de billets.
Quelles sont les sorties? Une liste des identifiants des billets de priorité 1, dans le même ordre. Une entrée vide donne [].
Que ne fait pas la fonction? Elle ne change pas la liste et n’affiche rien.

Appelez-la dans main() pour les billets sélectionnés et affichez le résultat. Avec les six billets et le statut "open" (ouvert), la nouvelle ligne est Urgent: ['T-1003'] (urgent : le billet T-1003).

Afficher une réponse
def urgent_ids(tickets):
"""Return the IDs of the tickets with priority 1, in order."""
return [ticket["id"] for ticket in tickets if ticket["priority"] == 1]

Dans main(), après la moyenne : print(f"Urgent: {urgent_ids(selected)}").

Erreur courante

Une liste par défaut qui garde les valeurs en mémoire

Symptôme : une fonction donne un résultat différent à chaque appel avec les mêmes arguments :

def add_problem(problem, problems=[]):
problems.append(problem)
return problems
print(add_problem("missing id")) # ['missing id']
print(add_problem("unknown status")) # ['missing id', 'unknown status']

Le deuxième appel devrait renvoyer seulement ['unknown status'] (statut inconnu).

Cause : Python crée la valeur par défaut une seule fois, quand il exécute la ligne def, pas à chaque appel. Une liste peut changer. Chaque appel sans cet argument reçoit donc la même liste, et chaque append y ajoute un élément. C’est le problème de référence partagée présenté dans Les collections, à un endroit moins visible.

Correction : utilisez None comme valeur par défaut et créez une nouvelle liste dans le corps :

def add_problem(problem, problems=None):
if problems is None:
problems = []
problems.append(problem)
return problems

Utilisez seulement des valeurs qui ne peuvent pas changer comme valeurs par défaut : nombres, chaînes, True, False et None. Jamais une liste, un dictionnaire ou un ensemble.

Vérifiez votre compréhension

Une fonction se termine par print(total) et n'a pas de return. Que donne result = the_function(...), et pourquoi est-ce un problème?

Afficher la réponse

result vaut None, car une fonction sans return renvoie None. Le total s’affiche à l’écran, mais le programme ne peut pas l’utiliser. Il ne peut pas le comparer, l’enregistrer ou le vérifier avec assert. Renvoyez la valeur et affichez-la là où vous appelez la fonction.

Vérifiez votre compréhension

Vérification du module, partie 1 : expliquez le contrat de filter_by_status en mots simples, puis prouvez-en deux éléments avec assert.

Afficher la réponse

Entrées : une liste de dictionnaires de billets et un statut (par défaut, "open"). Sortie : une nouvelle liste contenant seulement les billets ayant ce statut, dans le même ordre. Une entrée vide donne une liste vide. La fonction ne change pas la liste d’entrée et n’affiche rien. Deux vérifications : assert filter_by_status([]) == [] et, pour une liste avec un billet ouvert et un billet fermé, assert len(filter_by_status(two, "closed")) == 1.

Récapitulatif

  • Une fonction a des paramètres, un corps et un return. L’appel donne aux paramètres les valeurs des arguments et devient la valeur renvoyée.
  • Écrivez d’abord le contrat : les entrées, les sorties et ce que la fonction ne change pas.
  • Une fonction pure lit seulement ses entrées et renvoie un résultat. C’est le type de fonction le plus facile à vérifier avec assert.
  • Les variables d’une fonction sont locales. Les valeurs sortent d’une fonction seulement par return.
  • N’utilisez jamais une liste, un dictionnaire ou un ensemble comme valeur par défaut. Utilisez None, puis créez-en un nouveau dans le corps.

Ensuite, vous déplacez les fonctions du script vers un module pour que d’autres programmes puissent les importer.

Leçon suivante : Modules et importations