Autour du codeDevelopper toujours mieux
Posté le , mis à jour le

Les iterareur en python

Un itérateur c'est un objet qui est créé par un agrégat, un agrégat est n'importe quel objet composé de plusieurs choses comme une liste ou un wagon composé de passagers. L'itérateur va servir à parcourir cet objet, généralement grâce à deux méthodes next qui retourne le prochain élément et hasNext qui indique s'il reste un objet à retourner. Dans les langages au typage statique il y a généralement une classe abstraite iterator de laquelle on peut hériter.

L'impact du duck typing sur les iterators en python

  1. C'est du duck typing donc pas besoin d'hériter d'une classe Aggregate ou Iterator.
  2. Pas de méthode hasNext, un iterator lance une exception StopIteration
  3. La méthode next c'est __next__ (ou next dans python2)
  4. Pour créer un iterator, la méthode de l'agregate est __iter__

Le coup du __iter__ et __next__ en privé ça peut sembler bizarre, mais le truc c'est que c'est directement géré par la boucle for et qu'il existe les fonctions iter et next qui appele les méthodes privé de l'objet

Ainsi, le code ci-dessous:

lst = [1, 2, 3]
for i in lst:
    print(i)

Peut s'écrire comme ceci:

lst = [1, 2, 3]
iter_lst = iter(lst)
while True:
    try:
        print(next(iter_lst))
    except StopIteration:
        break

Écrire ses propres iterators en python

Pour faire ses propres itérateurs, on va prendre l'exemple d'une classe RGB sur laquelle on veut parcourir chaque valeur. On peut faire comme ça.

class RGB:
    # L'iterator est une nested class, il
    # n'a pas besoin d'être utilisé en dehors de RGB.
   class RGBIterator:
        def __init__(self, rgb):
            self._rgb = rgb  # On stoque l'agreggate.
            self._attrs = ['r','g', 'b'] # ça va nous servir à savoir quelle valeur on a déjà retourné.

        def __next__(self):
            if self._attrs:
                return getattr(self._rgb, self._attrs.pop(0))
            raise StopIteration()

        next = __next__ # Compatibilité python2/3

        def __iter__(self): # Un itérateur est généralement aussi un itérable
           return self      # en se retournant lui même

    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    def __iter__(self):
        return RGB.RGBIterator(self)

# Exemple d'utilisation:
rgb = RGB(64, 0, 18)
for i in rgb:
    print(i)

Mais il y a plus simple ! laughing En fait et il n'y a pas besoin d'implémenter __iter__. Du moment que la méthode __getitem__ est implémentée votre objet est itérable. __getitem__ sert à utiliser des [ ] sur votre objet comme dans my_obj[2] ou my_obj[a_key]

class RGB:

    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    def __getitem__(self, indice):
        return (self.r, self.g, self.b)[indice]

On peut également itérer dans l'autre sens avec la fonction reversed:

text = "elu par cette crapule"
for e in reversed(text):
    print(e)

Pour ça il faut que notre classe soit un iterator et qu'elle ait une méthode __len__ qui renvoie sa taille car si l'on part de la fin, il faut bien savoir par où commencer. wink

class RGB:
    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    def __getitem__(self, indice):
        return (self.r, self.g, self.b)[indice]

    def __len__(self):
        return 3

Bon remarquez que ce n'est pas très pertinent de faire un reversed sur un rgb et d'avoir un __len__. Mais c'est pour l'exemple.

Les itérateurs en python ne sont pas robustes

On parle de robustesse d'un itérateur lorsque celui-ci est capable de prendre en compte le fait que l'agrégat qu'il parcourt a changé. En python, si l'on parcoure une liste qui perd un élément, l'itérateur va se décaller.

Par exemple, si l'on supprime le nombre 12 dans la liste lors de l'itération, le nombre 13 n'est pas parcourue:

>>> l = list([11, 12, 13, 14])
>>> for e in l:
...    print(e)
...    if e == 12:
...        del l[1]
...
11
12
14

Rien ne nous empèche de créer une liste qui retourne un itérateur robuste. On va utiliser le design pattern observer, la liste sera observable et les itérateurs seront également observateurs.

class MyList:

    class MyListIterator:
        def __init__(self, my_list):
            self._my_list = my_list
            self._i = -1

        def __next__(self):
            self._i += 1
            try:
                return self._my_list[self._i]
            except IndexError:
                raise StopIteration

        def update(self, offset):
            if offset == self._i:
                self._i -= 1

    def __init__(self, iterable):
        self._list = list(iterable)
        self._iterators = []

    def __getitem__(self, key):
        return self._list[key]

    def __delitem__(self, key):
        del self._list[key]
        # Notifier les itérateurs qu'un élément est enlevé de la liste.
        for iterator in self._iterators:
            iterator.update(key)

    def __iter__(self):
        iterator = self.MyListIterator(self)
        self._iterators.append(iterator)
        return iterator

Le code d'exemple ci-dessus ne couvre pas le cas de la suppression de plusieurs éléments. Vous pouvez modifier la méthode MyListIterator.update pour qu'elle traite les objets de type slice.

Savoir si un objet est iterable

Un dernier truc, pour savoir si une classe est itérable, il y a la classe abstraite Iterable dans le module collections.

import collections
if isinstance(rgb, collections.Iterable):
    print(":-)")

Voilà, on a vu comment le design pattern iterator est intégré dans le langage python.