L'utilisation de types intégrés comme les dictionnaires et les tuples est courante en Python pour structurer rapidement des données. Cependant, lorsque la complexité d'un projet augmente, l'accumulation de ces structures (un dictionnaire contenant un dictionnaire de listes de tuples, par exemple) rend le code illisible et difficile à maintenir. Il devient alors impératif de refactoriser ces structures vers une hiérarchie de classes.
Le problème de l'imbrication croissante
Considérons un système simple pour enregistrer les notes d'étudiants. Initialement, un dictionnaire associant un nom à une liste de scores suffit.
class RegistreNotesSimple:
def __init__(self):
self._donnees = {}
def ajouter_etudiant(self, nom):
self._donnees[nom] = []
def enregistrer_note(self, nom, score):
self._donnees[nom].append(score)
def obtenir_moyenne(self, nom):
scores = self._donnees[nom]
return sum(scores) / len(scores)
# Utilisation
registre = RegistreNotesSimple()
registre.ajouter_etudiant('Alice')
registre.enregistrer_note('Alice', 85)
print(f"Moyenne de Alice : {registre.obtenir_moyenne('Alice')}")
Si nous devons maintenant classer les notes par matière, le dictionnaire _donnees doit contenir un autre dictionnaire.
class RegistreParMatiere:
def __init__(self):
self._donnees = {}
def ajouter_etudiant(self, nom):
self._donnees[nom] = {}
def enregistrer_note(self, nom, matiere, score):
etudiant = self._donnees[nom]
scores_matiere = etudiant.setdefault(matiere, [])
scores_matiere.append(score)
def moyenne_generale(self, nom):
etudiant = self._donnees[nom]
total_points = 0
nombre_notes = 0
for scores in etudiant.values():
total_points += sum(scores)
nombre_notes += len(scores)
return total_points / nombre_notes
# Utilisation
registre = RegistreParMatiere()
registre.ajouter_etudiant('Alice')
registre.enregistrer_note('Alice', 'Maths', 90)
registre.enregistrer_note('Alice', 'Algo', 80)
print(f"Moyenne générale : {registre.moyenne_generale('Alice')}")
L'ajout d'une pondérasion (coefficeint) pour chaque note rendrait la structure encore plus complexe : {nom: {matiere: [(note, poids), ...]}}. Accéder aux données via des indices numériques dans des tuples (ex: note[0]) est une source fréquente d'erreurs lors de l'évolution du code.
Refactorisation vers une architecture de classes
Pour résoudre ce problème, nous décomposons la structure en classes distinctes : Note, Matiere, Etudiant et CarnetDeNotes. L'utilisation de namedtuple de la bibliothèque collections est idéale pour représenter de petits objets de données immuables.
from collections import namedtuple
# Utilisation d'un namedtuple pour une structure de donnée légère et lisible
Evaluation = namedtuple('Evaluation', ('score', 'coefficient'))
class Matiere:
def __init__(self):
self._evaluations = []
def ajouter_note(self, score, coefficient):
self._evaluations.append(Evaluation(score, coefficient))
def moyenne(self):
total = 0
total_coeff = 0
for eval in self._evaluations:
total += eval.score * eval.coefficient
total_coeff += eval.coefficient
return total / total_coeff if total_coeff > 0 else 0
class Etudiant:
def __init__(self):
self._cursus = {}
def matiere(self, nom_matiere):
if nom_matiere not in self._cursus:
self._cursus[nom_matiere] = Matiere()
return self._cursus[nom_matiere]
def moyenne_globale(self):
if not self._cursus:
return 0
total_moyennes = sum(m.moyenne() for m in self._cursus.values())
return total_moyennes / len(self._cursus)
class GestionnaireScolaire:
def __init__(self):
self._repertoire = {}
def obtenir_etudiant(self, nom):
if nom not in self._repertoire:
self._repertoire[nom] = Etudiant()
return self._repertoire[nom]
# Exemple d'exécution
scolaire = GestionnaireScolaire()
alice = scolaire.obtenir_etudiant('Alice')
maths = alice.matiere('Mathématiques')
maths.ajouter_note(18, 0.2)
maths.ajouter_note(14, 0.8)
algo = alice.matiere('Algorithmique')
algo.ajouter_note(15, 1.0)
print(f"Moyenne de Alice en Maths : {maths.moyenne():.2f}")
print(f"Moyenne générale de Alice : {alice.moyenne_globale():.2f}")
Avantages de cette approche
Bien que cette version nécessite davantage de lignes de code, elle offre des bénéfices concrets :
- Lisibilité : Les noms de méthodes et d'attributs explicitent l'intention du code.
- Extensibilité : Si vous devez ajouter une règle de calcul spécifique ou de nouvelles métadonnées, vous modifiez une classe isolée sans impcater l'ensemble de la structure.
- Robustesse : L'utilisation de
namedtupleévite les erreurs d'indexation confuse (item[1]vsitem.coefficient).