CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
1CC1000 - Systèmes d'Information et Programmation - TD : Métro

Table des matières

L'objectif de ce TD est d'aborder quelques aspects du langage Python (fichiers, modules, tests, typage, affichage de cartes) dans le cadre d'un exercice autour du métro.

1. Introduction

Les données sur les transports en commun sont maintenant souvent accessibles comme données ouvertes (open data) afin de stimuler le développement d'applications innovantes par des acteurs autres que les opérateurs de ces réseaux de transport.

Par exemple, les données décrivant l’offre de transport en Île-de-France sont disponibles ici. Ces données sont volumineuses car elles couvrent de nombreux modes (métro, RER, bus, tramway…), et incluent de nombreuses informations comme les horaires.

Cet exercice portera sur une version simplifiée du réseau de métro, une extraction a été réalisée pour ne conserver que les informations utiles : noms des lignes de métro, noms et positions géographiques des stations de ces lignes.

2. Mise en place du projet

  • Créer un dossier vide qui sera la racine du projet.
  • Ouvrir ce dossier avec VSCode.
  • Créer un environnement et l'activer.
  • Créer un dossier data et enregistrer le fichier de données dans ce dossier.

Les colonnes de ce fichier sont : line_name, station_name, stop_lat, stop_lon ; les valeurs pour chaque ligne sont séparées par des virgules, les deux premieres colonnes sont des chaînes de caractères, les deux dernières des flottants utilisant le point décimal.

Les informations ainsi disponibles sont :

  • Les stations de métro : elles sont identifiées par un nom et hébergent des points d'arrêt.
  • Les lignes de métro : elles sont identifiées par un nom et constituées d'une liste ordonnée de points d'arrêt.
  • Les points d'arrêt : ils ont le nom de la station qui les hébergent, et des coordonnées géographiques. L'ordre des points d'arrêt dans le fichier est le même que l'ordre de ces points sur la ligne de métro.

3. Chargement des données

  • Créer un dossier metro_network qui contiendra le module chargé de lire le fichier de données et de mettre à disposition les informations.
  • Créer les classes MetroNetwork, MetroStation, MetroStop et MetroLine dans un fichier metro_network.py dans le dossier metro_network.
  • Le constructeur de MetroNetwork recevra le nom du réseau et le nom du fichier contenant les données de ce réseau.
  • Une fonction auxiliaire, appelée par ce constructeur, se chargera de lire les données et de créer les objets correspondants : vous déterminerez ainsi les attributs et arguments des constructeurs des autres classes.
  • La classe DictReader du module csv, sera utilisé pour lire le fichier de données.

La manipulation de fichiers est toujours compliquée quel que soit le langage de programmation utilisé : un fichier doit être ouvert avant d'accéder à son contenu, puis fermé, il faut tenir compte des erreurs potentielles (fichier qui n'est pas accessible), du codage utilisé dans le cas d'un fichier texte… En Python, les opérations de base sur les fichiers sont disponibles via le module io. D'autres modules offrent une surcoupe adaptée à des types de fichiers particulier, comme ici avec le module csv pour les fichiers dits « Comma Separated Values ».

L'instruction with de Python, utilisée dans l'exemple avec DictReader, permet de masquer cette complexité ; en particulier, le fichier sera systématiquement fermé après l'instruction with, y compris si une exception est émise au cours de l'utilisation du fichier.

Il faut noter que cette instruction with s'utilise de manière générale avec des ressources qui peuvent être autre chose que des fichiers (connexion à une base de données, connexion réseau…).

L'exemple avec DictReader ne précise pas le codage des caractères contenus dans le fichier de données, ce qui peut conduire à des erreurs de lecture ; vous devez donc préciser encoding="utf-8" dans l'appel à open().

4. Écriture de tests

  • Installer le module pytest dans votre environnement.

pytest est une bibliothèque facilitant l'écriture de tests unitaires.

  • Ajouter dans la classe MetroNetwork des observateurs sur le nombre de stations (number_stations) et le nombre de lignes (number_lines).
  • Créer dans le dossier racine un sous-dossier tests.
  • Créer dans ce nouveau dossier un fichier test_metro_data.py avec le contenu suivant :
from metro_network.metro_network import MetroNetwork


def test_empty_metro_network():
    network = MetroNetwork("Empty", "tests/empty_metro_stops.csv")
    assert network.name == "Empty"
    assert network.number_stations == 0
    assert network.number_lines == 0

  • Créer dans le même dossier un fichier empty_metro_stops.csv ne contenant que le nom des colonnes.

Si vous lancer simplement pytest, une erreur ModuleNotFoundError: No module named 'metro_data' sera affichée : il faut en effet informer pytest que le répertoire courant doit être utilisé pour la recherche de modules, ce qui peut se faire ainsi :

PYTHONPATH=. pytest
  • Vérifier que le test est réussi.
  • Prendre un extrait du fichier de données complet et écrire un autre test pour cet extrait.

5. Annotations de type

Python (comme Javascript) est un langage typé dynamiquement, ce qui signifie qu'une variable (ou le paramètre d'une fonction) peut être affectée par une donnée d'un type quelconque, et c'est uniquement à l'exécution que la combinaison de variables de types incompatibles est signalé par une erreur (par exemple, addition d'une chaîne de caractères et d'un nombre).

D'autres langages (Java, Typescript, C++…) sont typés statiquement : on précise le type des variables dans le code (par exemple, on déclare qu'une variable contiendra des entiers), et toute tentative d'y stocker une donnée d'un type non compatible sera détecté comme une erreur avant même l'exécution du code. Ce choix diminue le côté dynamique de ces langages, mais offre l'avantage de détecter des erreurs plus tôt dans un cycle de développement.

Il est possible en Python d'ajouter dans le code des annotations de types et d'utiliser des outils qui vont vérifier avant l'exécution que toute affectation d'une valeur dans une variable est conforme au type indiqué.

Par exemple, le constructeur de MetroStop reçoit la position géographique du point d'arrêt : est-ce que ces données sont des chaînes de caractères, telles que lues dans le fichier CSV, donc le constructeur de MetroStop devra faire la conversion en flottant, ou est-ce que ces données sont déjà sous la forme de nombres, donc c'est l'appelant du constructeur de MetroStop qui devra faire la conversion ?

Nous choisissons ici la seconde solution, car la construction des points d'arrêt pourrait se faire à partir d'une base de données (et non pas dans un fichier CSV), avec des positions géographiques directement stockées comme des nombres.

  • Créer un fichier test_type_annotation.py avec le contenu suivant :
def test_type_annotation(an_integer, a_string):
    return "".join(a_string for _ in range(an_integer))

print(test_type_annotation(3, "abc"))
print(test_type_annotation("abc", 3))
  • Exécuter ce script, vous obtenez une erreur : TypeError: 'str' object cannot be interpreted as an integer.
  • Modifier le fichier test_type_annotation.py en ajoutant des annotations de type :
def test_type_annotation(an_integer: int, a_string: str) -> str:
    return "".join(a_string for _ in range(an_integer))

print(test_type_annotation(3, "abc"))
print(test_type_annotation("abc", 3))

Vous pouvez constater que ces annotations sont acceptées par Pylint car elles sont prises en compte dans la définition de la syntaxe du langage (voir typing. La même erreur est affichée si vous exécutez ce script.

Pour activer la vérification de ces annotations avant l'exécution du code, il faut installer un outil supplémentaire : ajouter à VSCode l'extension Mypy Type Checker. Après activation de cette extension, VSCode vous signale l'erreur Argument 1 to "static_type" has incompatible type "str"; expected "int".

  • Ajouter des annotations de types dans le fichier MetroData.py :
    • On utilise -> None pour une fonction ou une méthode qui ne retourne rien.
    • On peut utiliser float comme type ; on peut aussi utiliser int | float pour indiquer que les deux sont possibles.
    • Un dictionnaire est annoté avec dict[key_type, value_type].
    • On n'annote pas self, ni le retour de __init__().
    • Une variable de boucle doit être annotée avant, par exemple :
            row: dict[str, str]
            for row in reader:
  • La documentation de Mypy est disponible ici

Si votre version de Python est inférieure à 3.14, ajouter la ligne suivante à votre fichier pour éviter une NameError:

from __future__ import annotations

6. Affichage

L'affichage de cartes est maintenant quelque chose de courant depuis l'apparition des smartphones. Leaflet est une bibliothèque Javascript très utilisée pour cela. Folium est une bibliothèque Python qui utilise Leaflet pour l'affichage.

Les cartes créées par Folium peuvent être affichées dans un Notebook Jupyter, sauvegardées dans un fichier html ou directement affichées dans un navigateur via un serveur web. Nous utiliserons cette dernière possibilité via Flask, un un micro-framework web léger en Python.

  • Installer Folium et Flask dans votre environnement Python.
  • Créer un dossier display dans votre dossier projet.
  • Créer un fichier display_network.py dans ce dossier avec le contenu suivant :
import folium
from flask import Flask

from metro_network.metro_network import MetroNetwork


def create_map_for_network(network: MetroNetwork) -> folium.Map:
    map: folium.Map = folium.Map((48.86, 2.35), zoom_start=13, tiles=None)
    folium.TileLayer("OpenStreetMap", overlay=True).add_to(map)

    folium.LayerControl().add_to(map)

    return map


app = Flask(__name__)

@app.route("/")
def display_map():
    return metro_map.get_root().render()

metro_map = None

if __name__ == "__main__":
    metro_map = create_map_for_network(MetroNetwork("Metro IdF", "data/idf_metro_stops.csv"))
    app.run()
  • Lancer votre application avec la commande suivante :
PYTHONPATH=. python display/display_network.py
  • Ouvrer une fenêtre de votre navigateur sur l'URL indiquée et vérifier le bon affichage de la carte de la région parisienne ; noter en particulier le contrôle en haut à droite des couches affichées.

Nous allons ajouter à cette carte initiale une couche qui contiendra le tracé des lignes (on se contentera de segments de droite entre les points d'arrêt) et une autre pour afficher les stations.

  • Définir une fonction create_station_layer(network: MetroNetwork) -> folium.FeatureGroup chargée de créer une couche pour l'affichage des stations ; cette fonction devra interroger l'objet réseau, ce qui vous amènera à ajouter des accesseurs dans vos classes du fichier metro_network/metro_network.py.
  • Appeler cette fonction dans create_map_for_network() avant l'ajout du LayerControl.

Pour rendre l'affichage plus lisible, vous pouvez utiliser des couleurs différentes pour les folium.Marker représentant les stations (noire pour les stations multi-lignes, sinon la couleur qui sera aussi choisie par la ligne).

Les couleurs suivantes sont disponibles :

    ['red', 'blue', 'green', 'purple', 'orange', 'darkred',
     'lightred', 'beige', 'darkblue', 'darkgreen', 'cadetblue',
     'darkpurple', 'white', 'pink', 'lightblue', 'lightgreen',
     'gray', 'black', 'lightgray']
  • Définir une fonction create_line_layer(network: MetroNetwork) -> folium.FeatureGroup chargée de créer une couche pour l'affichage des lignes.