itertools

lock logo

Par Johnny Da Costa 1

Introduction

Le module itertools de Python nous propose un bon nombre de générateurs prêts à l’emploi. Qu’est-ce qu’un générateur ou un itérateur ? Que permettent les itérateurs. Pour comprendre ça, nous allons utiliser un exemple très simple. Imaginez que vous avez une liste et que vous voulez l’afficher. La première chose qui nous vient à l’esprit est d’utiliser une boucle.

>>> liste = ["I", "do" "love", "programming", "in", "python"]
>>> for i in liste:
...     print(i)
I
do
love
programming
in
python

Derrière cette boucle for se cache enfet un itérateur. C’est un objet qui va être chargé de parcourir un objet conteneur, dans notre cas notre liste. Quand Python va tomber sur la ligne for i in liste:, il va appeler l’itérateur de notre liste pour pouvoir la parcourir. L’itérateur se crée dans la méthode __iter__ de notre objet liste dans notre cas. Et va nous retourner notre itérateur pour pouvoir parcourir notre petite liste.

A chaque itération python va appeler la méthode __next__ de notre liste pour itérer à l’élément suivant ou s’arrêter avec l’exception StopIteration si le parcours est fini.

Voici un autre exemple mais avec une chaine caractère.

>>> chaine = "Python"
>>> iterateur = iter(chaine) # va nous retourner un itérateur sur notre chaine
>>> print(next(iterateur)) # va nous afficher la première lettre de notre string
P
>>> for i in iterateur: # va parcourir tous les caractères un à un
...     print(i)
y
t
h
o
n

On voit que le P à déjà été consommé lors de l’appel à la fonction next(iterateur)

Itérateurs offerts par Python

Python nous offres des itertools qui sont des itérateurs qui nous permette d’itérer sur nous objets. Voici une petit liste non-exaustif de ce que nous propose se module :

  • count() : crée un itérateur qui va nous retourner des valeurs espacé de par 1 défault. Attention boucle inifni.

  • repeat() : va nous répeter les éléments n fois.

  • chain() : permet de concaténer des listes, chaine de caractère, etc…

  • filterfalse() : créer un itérateur qui va filtrer les éléments qui sont itérable et nous retourner seulement ceux qui réponde faux à notre prédicat

La liste est encore longue et le but ici n’est pas de voir chaque itertools mais de comprendre le concept d’itérateur. Rien de mieux que pour comprendre ce que sont les itérateur que de créer le notre.

Nos itertools perso

Nous allons ici créer une classe qui va nous retourner un itérateur qui va parcouir un liste de la fin jusqu’au début.

Exemple inspiré de la documentation Python : Iterators.

  • Première étape nous allons créer notre class qui va nous retourner un itérateur



class itRevListe:
    """
    Nous retourne un itérateur.

    Ce qui nous permettra de parcourir notre liste de la fin jusqu'au début.
    """

    def __init__(self, liste):
        """On va se mettre à la fin de notre liste."""
        self.liste = liste
        self.position = len(liste)

    def __next__(self):
        """On renvoie l'élément suivant dans notre liste."""
        if self.position == 0:
            raise StopIteration
        self.position -= 1
        return self.liste[self.position]


  • Deuxième étape remplacer l’itérateur de l’objet liste par le notre.



class revList(list):
    """
    On va reprendre les méthodes et attributs de la class list.

    Mais nous allons lui changer son itérateur par le notre.
    """

    def __iter__(self):
        """Renvoie notre iterateur perso."""
        return itRevListe(self)


  • troisième étape utiliser notre itertools perso!

>>> liste = revList(islice(count(), 0, 10))
>>> for i in liste:
...     print(i)
9
8
...

Les générateurs

Les générateurs et les itérateurs sont intimement liés. Pour faire simple, un générateur est une fonction construite à l’aide du mot clef yield. Mais contrairement aux fonctions habituelles, elle n’a pas de return, mais un ou plusieurs yield. Mais à quoi servent-ils ? Ce sont en fait des objets itérable, c’est à dire qu’ils vont à chaque passage nous renvoyé différentes valeurs. Pour les récupérer soit une boucle classique for … in … : ou bien la méthode next.

Un petit exemple simple :

>>> def say_hello(name):
...     yield "Bienvenue, "
...     yield                # return none
...     yield name
>>> for in say_hello("Johnny"):
...     print(i)
"Bienvenue,"
None
"Johnny"

Exemple de générateur fibonacci

D’après l’exemple de zeste de savoir

>>> def fibonacci(n, a=0, b=1):
...     for _ in range(n):
...         yield a
...         a, b = b, a + b

>>> list(fibonacci(10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

>>> list(fibonacci(5, 6, 7))
[6, 7, 13, 20, 33]

Exemple d’utilisations d”itertools

Essayons maintenant de résoudre un problème avec les itertools que Python nous offre. Imaginons que nous avons une liste de points qui formerait un chemin dont on aimerait connaitre la distance.

Objectif :

>>> chemin = [A, B, C]
>>> pairs = [(A, B), (B, C), (C, A)]

>>> distances = [len(B - A), len(C - B), len(A - C)]
>>> distance = sum(distances)

Voici nos fonctions :

def pairs(lst):
    """Pairs("abc") --> [(a,b), (b,c), (c,a)]."""
    i = cycle(lst)
    next(i)
    return list(zip(lst, i))


def getDistance(p):
    """Retourne la distance entre chaque pairs."""
    return list(chain(p[i][1]-p[i][0] for i in range(len(p))))

Sortie :

>>> chemin = [5, 3, 23, 223]
>>> steps = pairs(chemin)
>>> print(steps)
[(5, 3), (3, 23), (23, 223), (223, 5)]
>>> distances = getDistance(steps)
>>> distances
[-2, 20, 200, -218]
>>> sum(distances)
0

Un peu de design pattern

Tout le monde utilise ce modèle de conception sans même le connaître vraiment. Imaginez le cas on nous voudrions itérer sur un répertoire pour obtenir son contenu ou appliquer un traitement sur chacun des fichiers. Sans l’approche itérateur, ils nous faudraient recupérer un flux sur le dossier courant est utiliser un while pour parcourir tout notre arborescence et vérifier le type (fichier ou dossier ?) et ensuite afficher. Mais imaginons que notre problème change et que nous ne devons plus itérer sur un répertoire mais sur un autre type de structure… Et on est repartie pour réecrire du code fastidueux et compliqué.

Avec l’utilisation d’itérateur notre programme devient tout de suite plus robuste et plus élégant. Un gros avantage avec ce pattern est que si notre structure change, il nous suffit d’adapter son comportement sur cette objet (décorateur) pour lui indiquer comment itérer sur cette objet. Grâce à ça, nous cachons la compléxité de parcours à notre client qui ne se rend même pas compte de ce qui se passent réelement.

Mais en Python ce patron de conception est complétement intégré au language. Ce qui nous simplifie grandement la vie!

Voici un exemple de parcours du contenu d’un répertoire.

>>> for element in os.listdir('./'):
...     if os.path.isdir(element):
...         print(f"'{element}' un dossier")
...     else:
...         print(f"'{element}' est un fichier")

Conclusion

itertools est un module permettant de faire des choses simpas avec cet objet qu’est l’itérateur. Ils sont tellement utile et important que Python a dédié un module pour les opérations d’itération qui sont les itertools. L’itérateur apporte un niveau d’abstraction (couche de code en plus pour réaliser une action.) L’avantage est que l’itérateur est un objet qui coûte peu en utilisation mémoire. La syntaxe est peu plus élégante car on va masquer au client la complexité de notre code. Les itérateurs nous permettent d’itérer sur toute sorte de structure de données, ce qui rend notre code plus robuste et reutilisable.

1

<johnny.dacosta@he-arc.ch>