Cours Python · Module 2: Structures de données

Projet : Traqueur d'espèces de safari

8 min de lecture
Dans cette leçon8

Bienvenue ! Après des semaines sur le sentier, ton journal déborde, et l'équipe n'arrête pas de demander : quels animaux sont menacés, lequel est le plus lourd, avons-nous déjà vu un zèbre ? Fouiller les notes à la main prend des heures, alors qu'un programme répond en une fraction de seconde. Il est temps de construire le Traqueur d'espèces de safari (Safari Species Tracker), un outil qui réunira, étape par étape, tout ce que tu as appris dans ce module.

Étape 1 : la fiche d'une espèce

Nous décrivons chaque espèce avec les mêmes rubriques, comme une fiche dans le catalogue des découvertes. Un dictionnaire est parfait pour cela, car chaque information porte un nom lisible :

1lion = {
2    "name": "Lion",
3    "category": "mammifère",
4    "weight_kg": 190,
5    "endangered": True,
6    "sightings": 5
7}

Nous écrivons les clés en anglais, comme les noms de variables, alors que les valeurs peuvent être en français. L'essentiel est que chaque fiche ait les mêmes clés, car le code peut alors accéder sans risque à ["weight_kg"] pour n'importe quelle espèce. S'il manquait cette rubrique à une seule fiche, le programme s'arrêterait sur une erreur KeyError.

Étape 2 : la base des espèces

Nous construisons la base comme une liste de dictionnaires, dans laquelle l'ordre des fiches suit l'ordre des découvertes :

1database = [
2    lion,
3    {"name": "Zèbre", "category": "mammifère", "weight_kg": 350, "endangered": False, "sightings": 12},
4    {"name": "Flamant", "category": "oiseau", "weight_kg": 3, "endangered": False, "sightings": 25},
5    {"name": "Crocodile", "category": "reptile", "weight_kg": 450, "endangered": False, "sightings": 7}
6]
7print(database[1]["name"])  # Zèbre

Lis database[1]["name"] de gauche à droite : l'indice de la liste choisit la deuxième fiche, puis la clé du dictionnaire y choisit le nom. Tu ajoutes une nouvelle espèce avec la méthode append(), que tu connais déjà :

1database.append({"name": "Éléphant", "category": "mammifère", "weight_kg": 5000,
2                 "endangered": True, "sightings": 3})
3print(len(database))  # 5

L'éléphant est arrivé à la fin de la base, et les fiches précédentes sont restées à leur place.

Étape 3 : les rapports

La base commence maintenant à répondre aux questions. Une compréhension de liste parcourt toutes les fiches et prend dans chacune ce dont tu as besoin, tandis que la condition if ne garde que les fiches qui correspondent :

1names = [s["name"] for s in database]
2endangered = [s["name"] for s in database if s["endangered"]]
3print(names)       # ['Lion', 'Zèbre', 'Flamant', 'Crocodile', 'Éléphant']
4print(endangered)  # ['Lion', 'Éléphant']

La variable s représente tour à tour chaque fiche de la base. Les deux listes sont nouvelles, donc la base n'a pas changé du tout. Tu calcules les statistiques de la même façon : la fonction sum() additionne les nombres d'une liste, et len() donne le nombre de fiches.

1total_sightings = sum([s["sightings"] for s in database])
2average_weight = sum([s["weight_kg"] for s in database]) / len(database)
3print(total_sightings)  # 52
4print(average_weight)   # 1198.6

Le poids moyen atteint presque 1200 kg, parce que l'éléphant fait fortement grimper le résultat. La question des catégories d'animaux porte sur des valeurs uniques, donc la réponse est un ensemble. Il suffit de remplacer les crochets par des accolades :

1categories = {s["category"] for s in database}
2print(sorted(categories))  # ['mammifère', 'oiseau', 'reptile']

Trois mammifères ont donné une seule entrée 'mammifère' dans l'ensemble. Un ensemble n'a pas d'ordre, c'est pourquoi je l'ai passé dans la fonction sorted() pour l'affichage : elle renvoie une liste ordonnée.

Étape 4 : une fonction comme clé

La question de l'animal le plus lourd demande de comparer les fiches selon leur poids. Les fonctions max(), min() et sorted() acceptent un paramètre key : une fonction qui donne, pour chaque fiche, la valeur à comparer. Le plus court est de l'écrire sous forme de lambda, une fonction d'une ligne sans nom. Lis lambda s: s["weight_kg"] ainsi : prends la fiche s et rends son poids.

1heaviest = max(database, key=lambda s: s["weight_kg"])
2lightest = min(database, key=lambda s: s["weight_kg"])
3print(heaviest["name"])  # Éléphant
4print(lightest["name"])  # Flamant

La fonction max() compare les poids, mais elle renvoie la fiche entière de l'éléphant, pas seulement son poids. Tu pourrais écrire la même lambda avec une fonction ordinaire, def get_weight(s): return s["weight_kg"], et la passer sous la forme key=get_weight, sans parenthèses. Cela fonctionne de façon identique, mais la lambda n'encombre pas le programme avec un nom utilisé une seule fois, c'est pourquoi je te la recommande pour les clés simples.

Étape 5 : le tri

La même clé trie toute la base, et le paramètre reverse=True range les fiches en commençant par la plus lourde :

1by_weight = sorted(database, key=lambda s: s["weight_kg"], reverse=True)
2ranking = [s["name"] for s in by_weight]
3print(ranking)  # ['Éléphant', 'Crocodile', 'Zèbre', 'Lion', 'Flamant']

La fonction sorted() a renvoyé une nouvelle liste, donc database garde toujours les fiches dans l'ordre des découvertes. La méthode database.sort() avec la même clé marcherait aussi, mais elle écraserait cet historique. Et si tu veux regrouper les animaux par catégorie, puis ranger chaque groupe du plus léger au plus lourd ? La clé peut renvoyer un tuple, et Python compare les tuples élément par élément : d'abord la catégorie, puis, en cas d'égalité, le poids.

1grouped = sorted(database, key=lambda s: (s["category"], s["weight_kg"]))
2order = [s["name"] for s in grouped]
3print(order)  # ['Lion', 'Zèbre', 'Éléphant', 'Flamant', 'Crocodile']

Les mammifères, l'oiseau et le reptile suivent l'ordre alphabétique de leurs catégories, et les trois mammifères se sont rangés du lion à l'éléphant. Le tuple, que tu connais comme un relevé immuable, sert ici de clé de tri à deux niveaux.

Étape 6 : la recherche

L'équipe demande si le zèbre est déjà dans la base. La fonction ci-dessous parcourt les fiches une par une et renvoie la première qui correspond. La méthode de chaîne lower() met les lettres en minuscules, donc « zèbre » et « Zèbre » signifient la même chose.

1def find_species(database, name):
2    for species in database:
3        if species["name"].lower() == name.lower():
4            return species
5    return None

Dès que return rend une fiche, la fonction se termine. C'est seulement quand la boucle a parcouru toute la base sans rien trouver que nous renvoyons None. Nous passons la base en paramètre, donc la fonction marchera avec n'importe quelle liste de fiches.

Étape 7 : le journal d'observations

Chaque observation sur le terrain est une paire : une espèce et un lieu. Une telle entrée ne doit pas changer, donc nous la stockons dans un tuple, et toute la journée dans une liste de tuples. L'écriture x += 1 est un raccourci de x = x + 1 :

1log = [("Zèbre", "point d'eau"), ("Lion", "savane"), ("Zèbre", "rivière")]
2for animal, place in log:
3    found = find_species(database, animal)
4    if found is not None:
5        found["sightings"] += 1
6print(database[1]["sightings"])  # 14

La boucle dépaquette chaque tuple en animal et place, et la condition is not None ne laisse passer que les espèces de la base. La fiche trouvée est le même dictionnaire que celui de la base, pas une copie, c'est pourquoi le zèbre a maintenant 14 observations au lieu de 12. Ce sont les fiches qui changent, pas les tuples : les entrées du journal restent telles que le pisteur les a notées.

En fin de journée, je pose une question à l'équipe : quelles espèces de la base n'avons-nous pas vues aujourd'hui ? C'est la différence de deux ensembles, et une compréhension dépaquette un tuple exactement comme une boucle :

1seen_today = {animal for animal, place in log}
2all_species = {s["name"] for s in database}
3print(all_species - seen_today)  # {'Flamant', 'Crocodile', 'Éléphant'}

Le zèbre apparaît deux fois dans le journal, mais une seule fois dans l'ensemble seen_today. L'opérateur - a gardé les espèces absentes du journal, et ce sont elles que les pisteurs chercheront demain.

Étape 8 : le menu

Il reste le menu, grâce auquel toute l'équipe pourra utiliser le traqueur. La boucle while True demande un choix avec input(), et break termine le programme :

1while True:
2    choice = input("1 - menacées, 2 - chercher, 0 - quitter : ")
3    if choice == "1":
4        print(endangered)
5    elif choice == "2":
6        name = input("Espèce : ")
7        print(find_species(database, name))
8    elif choice == "0":
9        break

Nous comparons le choix au texte "1", et non à un nombre, car input() renvoie toujours une chaîne. Chaque option appelle quelque chose que tu as déjà, donc le menu reste court, et une nouvelle option, par exemple le tri, n'est qu'une nouvelle branche elif.

Dans les exercices qui arrivent, tu construiras le traqueur étape par étape, puis tu enverras le programme terminé à ton mentor. Retiens : un bon traqueur repose sur des structures bien choisies, un dictionnaire pour la fiche, une liste pour la base, un tuple pour une entrée de terrain et un ensemble pour les questions sur les espèces uniques.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelle opération modifie la liste d'origine ?

  2. 2. Que se passe-t-il quand tu utilises dict['key'] et que la clé n'existe pas ?

  3. 3. Quelle structure de données choisirais-tu pour stocker des éléments uniques ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un dictionnaire appelé « species_template » avec les clés : name, category, weight, dangerous, habitat

  • Éditeur de code

    Crée une liste appelée « species_list » avec 3 espèces. Chaque espèce est un dictionnaire avec les mêmes clés que species_template : name, category, weight, dangerous, habitat

  • Éditeur de code

    Écris une fonction find_species(database, name) qui parcourt la liste de dictionnaires database et renvoie le dictionnaire de l'espèce portant ce nom, ou None si cette espèce n'existe pas

  • Tri horizontal

    Ordonne les éléments dans le bon ordre :

  • Éditeur de code

    Crée une liste species_list de 3 espèces (dictionnaires avec les clés name et weight), trie-la avec sorted() et le paramètre key de l'espèce la plus légère à la plus lourde selon la clé 'weight', et stocke le résultat dans la variable by_weight

  • Éditeur de code

    Crée un Safari Species Tracker complet : une liste de dictionnaires d'espèces, au moins 3 fonctions (def) - ajout d'une espèce (append), recherche par nom et tri par poids (sorted) - et un menu dans une boucle while qui lit le choix avec input() ; l'option 0 termine le programme

  • Tri horizontal

    Ordonne les éléments dans le bon ordre :

  • Tri vertical

    Ordonne les étapes de l'algorithme du tri à bulles dans l'ordre :

Articles utiles