Python : supprimer des éléments pendant une boucle

Ta boucle Python laisse des éléments après remove ? Suis les indices, filtre sans modifier l'entrée et choisis quand mettre la liste à jour en place.

PythonPar
Publié le

Tu supprimes les nombres pairs dans une boucle for, mais un 4 reste dans la liste. Aucun message d'erreur. Le résultat dépend de la place des valeurs que tu retires.

En bref

Pour supprimer des éléments d'une liste Python selon une condition, construis une nouvelle liste avec une compréhension. Retirer un élément avec remove pendant que tu parcours cette même liste décale les suivants : la boucle peut en sauter. Si les autres références doivent voir la modification, affecte le résultat à valeurs[:] plutôt qu'au nom valeurs.

Pourquoi remove saute-t-il des éléments ?

L'itérateur de liste avance vers l'indice suivant, alors qu'une suppression déplace les éléments. Voici un exemple avec deux nombres pairs voisins. enumerate affiche le numéro du tour, qui correspond ici à l'indice demandé par l'itérateur. La liste est affichée avant chaque suppression.

skip.pyÀ éviter
valeurs = [2, 4, 5, 6]for index, x in enumerate(valeurs):    print(index, x, valeurs)    if x % 2 == 0:        valeurs.remove(x)print(valeurs)
Terminal
$ python3 skip.py0 2 [2, 4, 5, 6]1 5 [4, 5, 6]2 6 [4, 5, 6][4, 5]

Au premier tour, le 2 disparaît. Le 4 glisse de l'indice 1 à l'indice 0, mais le parcours continue à l'indice 1 : il y trouve le 5. Le 4 ne sera pas testé.

TourListe avant le testValeur lueAprès suppression éventuelle
0[2, 4, 5, 6]2[4, 5, 6]
1[4, 5, 6]5[4, 5, 6]
2[4, 5, 6]6[4, 5]

Le problème ne vient pas d'une copie créée par for. L'itérateur consulte la liste qui change. Un exemple avec des pairs toujours séparés par des impairs peut sembler fonctionner ; il ne vérifie pas le cas des suppressions consécutives.

Comment filtrer sans modifier la liste d'entrée ?

Une compréhension crée une nouvelle liste avec les valeurs à garder. Pour enlever les pairs, garde les valeurs dont le reste de la division par 2 n'est pas nul.

filter.py
valeurs = [2, 4, 5, 6]result = [x for x in valeurs if x % 2 != 0]print(result)print(valeurs)print(result is valeurs)
Terminal
$ python3 filter.py[5][2, 4, 5, 6]False

Le résultat contient 5 et l'entrée contient encore ses quatre valeurs. Le test is affiche False car les deux noms désignent des listes distinctes. Les doublons conservés restent dans leur ordre d'origine. Une entrée vide produit une nouvelle liste vide.

Ce contrat compte quand tu écris une fonction appelée ailleurs : le code qui lui passe une liste peut avoir besoin de la réutiliser. Dans la recherche dichotomique, le parcours garde également le tableau intact ; ici, on construit un résultat séparé.

Python

merge_sorted_values

8-bit

Fusionne deux listes d'entiers déjà triées sans modifier les entrées. Ce challenge ne demande pas de filtrer : il fait travailler le même choix de construire un résultat tout en parcourant des données qui doivent rester intactes. Conserve aussi les doublons.

Ouvrir l'exercice

Quelle différence entre valeurs = ... et valeurs[:] = ... ?

L'affectation au nom remplace sa référence. L'affectation à la tranche remplace le contenu de la liste existante, que les autres références continuent à voir.

aliases.py
valeurs = [2, 4, 5, 6]alias = valeursvaleurs = [x for x in valeurs if x % 2 != 0]print(valeurs, alias, valeurs is alias)
valeurs = [2, 4, 5, 6]alias = valeursvaleurs[:] = [x for x in valeurs if x % 2 != 0]print(valeurs, alias, valeurs is alias)
Terminal
$ python3 aliases.py[5] [2, 4, 5, 6] False[5] [5] True

Dans la première moitié, alias garde la liste d'origine. Dans la seconde, les deux noms désignent toujours le même objet ; le contenu filtré est visible depuis les deux noms. Choisis selon le contrat attendu, pas selon la forme la plus courte.

La compréhension à droite de l'affectation par tranche est calculée avant la modification. Elle évite donc de supprimer des éléments au milieu de son propre parcours. Elle alloue tout de même une liste temporaire : modifier la liste d'origine ne signifie pas travailler sans mémoire supplémentaire.

Peut-on parcourir une copie et utiliser remove ?

Oui, si ton traitement a besoin d'une boucle avec plusieurs actions, parcourir une copie sépare les éléments visités de la liste modifiée.

snapshot.py
valeurs = [2, 2, 4, 5, 6]for x in valeurs.copy():    if x % 2 == 0:        valeurs.remove(x)print(valeurs)
Terminal
$ python3 snapshot.py[5]

La copie fournit les deux occurrences de 2, même après le retrait de la première dans valeurs. remove(x) retire la première occurrence égale à x dans la liste cible. Ce code suppose que seule cette suppression modifie la liste pendant le traitement ; une autre action qui retire déjà x pourrait faire échouer remove avec ValueError.

Pour un simple filtrage, préfère la compréhension. La copie puis les recherches et décalages répétés de remove font davantage de travail. Une copie de liste reste superficielle : si les éléments sont eux-mêmes des listes, leurs contenus ne deviennent pas indépendants.

Questions fréquentes

Pourquoi enumerate ne corrige-t-il pas le problème ?

enumerate ajoute un compteur au parcours. Il ne fige pas les positions de la liste et n'empêche pas les décalages provoqués par une suppression.

Que faire avec une liste vide ou des doublons ?

La compréhension accepte une liste vide. Elle teste chaque occurrence et garde les doublons qui satisfont la condition, dans leur ordre d'origine.

Puis-je utiliser del à la place de remove ?

del valeurs[i] supprime par indice, mais décale aussi les éléments suivants. Dans un parcours vers l'avant, remplacer remove par del ne règle pas ce décalage.

Affecter valeurs = valeurs.copy() avant la boucle suffit-il ?

Non si tu parcours ensuite valeurs et modifies encore valeurs : tu parcours et modifies la même nouvelle liste. La boucle doit parcourir une copie séparée, comme dans snapshot.py.

Sources

À toi d'écrire le code.