Itérateurs en Python

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Ce billet est le premier d'une série en quatre volets. Voir le volet 2, le volet 3 ou le volet 4.

Je lis pas mal de choses sur asyncio en ce moment. Du coup au fil de mes lectures j'ai décidé de mettre au propre mes notes sur différentes briques qui touchent au sujet.

Je parlerais dans les prochaines semaines des générateurs simples, des generator expressions, des coroutines et de l'event loop.

Mais je commence par la base de chez base avec les itérateurs en Python. Peut-être que je couvrirais plus tard les itérateurs en JavaScript mais c'est grave moins sexy :p

Itérateurs

En informatique théorique, un itérateur est un patron de conception qui fournit une interface uniforme permettant la traversée d'une structure agrégat.

En Python, on parle d'iterator dont le protocole est décrit dans la PEP 234, de container plutôt que de structure agrégat et d'iterable pour désigner un objet que l'on peut parcourir.

Plus précisément :

  • un container est un objet :
    • contenant d'autres objets
    • supportant les tests d'appartenance
    • stocké en mémoire dans son entièreté (voir aussi collections)
  • un iterable est un objet :
    • sur lequel on peut itérer (via une boucle for par exemple)
    • dont il est possible d'obtenir un iterator via la fonction prédéfinie iter()
  • un iterator est un objet :
    • qui est aussi un iterable
    • disposant d'un état interne pour se souvenir de sa position lors d'une itération
    • dont il est possible d'obtenir le prochain élément via la fonction prédéfinie next()

Quelques remarques :

  • un container n'est pas forcément un iterable d'un point de vue purement théorique, mais en pratique il l'est quasiment toujours
  • un iterable peut être son propre iterator
  • un iterator n'est parcouru qu'une seule fois car il met à jour son état interne au fur et à mesure
  • un iterator est performant car chaque itération réutilise le même espace mémoire

Sous le capot c'est le protocole des itérateurs qui nous permet d'itérer de la même façon sur des objets différents :

  • un iterable doit fournir une méthode .__iter__() qui retourne un objet iterator
  • un iterator doit :
    • fournir une méthode .__next__() qui met à jour l'état interne de position et retourne le prochain élément du conteneur
    • lever StopIteration quand il n'y a pas de prochain élément pour signaler la fin de l'itération
    • fournir une méthode .__iter__() qui retourne self afin d'être lui-même un iterable et de pouvoir être utilisé par tout ce qui consomme un iterable

La boucle for utilise implicitement ce protocole :

for num in range(5):
    print(num)

On pourrait écrire la boucle précédente "à la main" :

nums = range(5)

iterator = iter(nums)  # Get a brand new iterator.

while True:
    try:
        num = next(iterator)
    except StopIteration:
        break
    # Body of for loop.
    print(num)

Notons pour finir qu'un itérateur peut servir à faire de l'évaluation paresseuse, on parle parfois de streams, et peut très bien agir sur un flux de données infinies :

import random

for num in iter(lambda: random.randint(2, 10), 1):
    print(num)

Du reste, la littérature sur le sujet est abondante :

Vues des dictionnaires

En Python 3, les méthodes .keys(), .values() et .items() des dictionnaires ne retournent plus d'itérateurs mais des dictionary view objects (cf. PEP 3106 et rétroportage 2.7).

Ce concept est inspiré des Collection Views de Java, dixit Guido.

Ce qu'il faut retenir des dictionary views :

  • ce ne sont pas des iterators mais des iterables, il faut (hors contexte d'une boucle for) utiliser iter(d.keys()) en Python 3 pour un comportement similaire à d.iterkeys() en Python 2
  • il s'agit d'une vue dynamique ; quand le dictionnaire sous-jacent change, la vue reflète le changement
  • les tests d'appartenance sont supportés
  • la vue .keys() se comporte comme un set
  • la vue .items() se comporte comme un set si toutes les valeurs du dictionnaire sont hashables, de sorte que toutes les paires (clé, valeur) du dictionnaire soient uniques et hashables
  • la vue .values() ne se comporte jamais comme un set puisque les valeurs du dictionnaire ne sont pas uniques généralement

Par exemple :

In [1]: d = dict(a=1, b=2, c=3, d=4, e=5)

In [2]: k = d.keys()

In [3]: 'b' in k
Out[3]: True

In [4]: del d['b']

In [5]: 'b' in k
Out[5]: False

In [6]: type(k)
Out[6]: dict_keys

In [7]: s = set('azerty')

In [8]: k ^ s == s.symmetric_difference(k)
Out[8]: True

In [9]: [item for item in d.values() if item % 2 != 0]
Out[9]: [3, 1, 5]

Avant Voir plus de warnings dans la console Django Après Générateurs en Python

Tag Kemar Joint