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éfinieiter()
- sur lequel on peut itérer (via une boucle
- 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()
- qui est aussi un
Quelques remarques :
- un
container
n'est pas forcément uniterable
d'un point de vue purement théorique, mais en pratique il l'est quasiment toujours - un
iterable
peut être son propreiterator
- 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 objetiterator
- 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 retourneself
afin d'être lui-même uniterable
et de pouvoir être utilisé par tout ce qui consomme uniterable
- fournir une méthode
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 :
- Iterators — The Python Tutorial
- Iterators — Python Functional Programming HOWTO
itertools
— Functions creating iterators for efficient looping- Iterables, Iterators and Generators: Part 1 — Ian Ward
- Iterators — Richard E. Pattis (via)
- New Iterables in Python 3.0 — Python 3 increased use of iterators
- Low-level iteration — Ned Batchelder: Loop Like A Native
- Understanding Python's "for" statement
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 desiterables
, il faut (hors contexte d'une bouclefor
) utiliseriter(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 unset
- la vue
.items()
se comporte comme unset
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 unset
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]