Lorsqu'une boucle for itère sur un objet mutable, il ne faut pas modifier le sujet de la boucle.
Le code ci-dessous produira une erreur
# on souhaite enlever de l'ensemble toutes les chaînes # qui ne contiennent pas 'bert' ensemble = {'marc', 'albert'} # ce code ne fonctionne pas et provoque une exception # RuntimeError: Set changed size during iteration for valeur in ensemble: if 'bert' not in valeur: ensemble.discard(valeur)
Dans ce cas, une bonne solution serait de penser à une compréhension d'ensemble:
# creation d'un ensemble en compréhension ensemble2 = {valeur for valeur in ensemble if 'bert' in valeur}
C'est sans doute la meilleure solution. Par contre, évidemment, on n'a pas modifié l'objet ensemble initial, on a créé un nouvel objet. En supposant que l'on veuille modifier l'objet initial, il nous faut faire la boucle sur une shallow copy de cet objet. Notez qu'ici, il s'agit d'économiser de la mémoire, puisque l'on fait une shallow copy.
from copy import copy # on veut enlever de l'ensemble toutes les chaînes # qui ne contiennent pas 'bert' ensemble = {'marc', 'albert'} # si on fait d'abord une copie tout va bien for valeur in copy(ensemble): if 'bert' not in valeur: ensemble.discard(valeur) print(ensemble)
Ne vous fiez pas forcément à cet exemple, il existe des cas – nous en verrons plus loin dans ce document – où l'interpréteur peut accepter votre code alors qu'il n'obéit pas à cette règle, et du coup essentiellement se mettre à faire n'importe quoi.
Pour être tout à fait clair, lorsqu'on dit qu'il ne faut pas modifier l'objet de la boucle for, il ne s'agit que du premier niveau.
On ne doit pas modifier la composition de l'objet en tant qu'itérable, mais on peut sans souci modifier chacun des objets qui constitue l'itération.
Ainsi cette construction par contre est tout à fait valide :
>>> liste = [[1], [2], [3]] >>> print('avant', liste) avant [[1], [2], [3]] >>> for sous_liste in liste: ... sous_liste.append(100) >>> print('après', liste) après [[1, 100], [2, 100], [3, 100]]
Dans cet exemple, les modifications ont lieu sur les éléments de liste, et non sur l'objet liste lui-même, c'est donc tout à fait légal.
Pour bien comprendre la nature de cette limitation, il faut bien voir que cela soulève deux types de problèmes distincts.
D'un point de vue sémantique, si l'on voulait autoriser ce genre de choses, il faudrait définir très précisément le comportement attendu.
Considérons par exemple la situation d'une liste qui a 10 éléments, sur laquelle on ferait une boucle et que, par exemple au 5ème élément, on enlève le 8ème élément. Quel serait le comportement attendu dans ce cas ? Faut-il ou non que la boucle envisage alors le 8-ème élément ?
La situation serait encore pire pour les dictionnaires et ensembles pour lesquels l'ordre de parcours n'est pas spécifié ; ainsi on pourrait écrire du code totalement indéterministe si le parcours d'un ensemble essayait:
On le voit, il n'est déjà pas très simple d'expliciter sans ambiguïté le comportement attendu d'une boucle for qui serait autorisée à modifier son propre sujet.
Voyons maintenant un exemple de code qui ne respecte pas la règle, et qui modifie le sujet de la boucle en lui ajoutant des valeurs:
# cette boucle ne se termine pas liste = [1, 2, 3] for c in liste: if c == 3: liste.append(c)
Si vous essayez d'exécuter ce code sur votre ordinateur vous constaterez que la boucle ne termine pas: en fait, à chaque itération on ajoute un nouvel élément dans la liste, et du coup la boucle à un élément de plus à balayer; ce programme ne termine théoriquement jamais. En pratique, ce sera le cas quand votre système n'aura plus de mémoire disponible…