Gestion de notes : liste partagée entre deux classes
Un professeur de NSI écrit un petit programme pour gérer les notes de deux classes. Il crée notes = [0] * 5, puis classes = [notes, notes]. Il affecte classes[0][0] = 12 et constate que classes[1][0] vaut aussi 12. Plus loin, il définit :
def ajouter(note, liste=[]):
liste.append(note)
return liste
Les deux premiers appels ajouter(12) et ajouter(15) renvoient [12] puis [12, 15], alors qu'il attendait [12] puis [15]. Il corrige avec liste=None et un test, mais se demande ensuite pourquoi copie = notes[:] protège les nombres et non les sous-listes. Il veut aussi savoir si classes.copy() suffit pour dupliquer les deux classes.
1. Expliquer l'aliasing dans `classes = [notes, notes]` et représenter l'état mémoire après `classes[0][0] = 12`. Pourquoi `notes[:]` ne suffit-il pas si les éléments sont des listes ?
Read the worked answer
classes = [notes, notes] ne crée pas deux listes : cela crée une liste de deux références vers le même objet liste. En mémoire, classes[0] et classes[1] contiennent la même adresse. Modifier classes[0][0] modifie l'objet pointé, donc classes[1][0] voit la même case. On peut représenter cela par classes -> [ * , * ] où les deux étoiles pointent vers la même liste notes, dont la première case vaut 12. notes[:] crée une copie superficielle : la nouvelle liste contient les mêmes références vers les éléments. Si les éléments sont des entiers, comme 0, la copie est suffisante car les entiers sont immuables. Si les éléments sont des listes, les deux listes externes pointent vers les mêmes sous-listes. Il faut une copie profonde, par exemple copy.deepcopy(notes), ou construire [[0]*5 for _ in range(2)], qui crée une nouvelle liste interne à chaque tour.
2. Expliquer le comportement de `ajouter` avec l'argument par défaut mutable, puis proposer une correction et un test qui montre que deux appels indépendants ne partagent plus la liste.
Read the worked answer
La valeur par défaut [] est évaluée une seule fois, à la définition de la fonction. Tous les appels qui ne fournissent pas liste réutilisent le même objet. Le premier appel ajoute 12 dans cet objet, le deuxième ajoute 15 dans le même objet, d'où [12, 15]. La correction est :
def ajouter(note, liste=None):
if liste is None:
liste = []
liste.append(note)
return liste
On teste ajouter(12) == [12] et ajouter(15) == [15], et on vérifie que deux appels successifs ne partagent pas la liste. On peut aussi passer explicitement une liste à chaque appel, mais le None est plus sûr.
3. Le professeur veut créer une matrice 2×5 de zéros indépendants. Comparer `[[0]*5]*2`, `[[0]*5 for _ in range(2)]` et `[[0 for _ in range(5)] for _ in range(2)]`. Indiquer laquelle est correcte et pourquoi.
Read the worked answer
[[0]*5]*2 crée une liste externe de deux références vers la même liste interne : modifier une ligne modifie l'autre. C'est le même piège que classes. [[0]*5 for _ in range(2)] crée une nouvelle liste [0]*5 à chaque itération, donc deux lignes indépendantes. Les zéros internes sont des entiers immuables, donc le partage des entiers n'est pas un problème. [[0 for _ in range(5)] for _ in range(2)] est également correct et crée explicitement chaque cellule. La deuxième forme est la plus lisible pour une matrice de zéros. Si les cellules devaient contenir des listes, il faudrait une compréhension imbriquée créant chaque sous-liste, ou deepcopy.