4.3. Les itérateurs personnalisés

Afin qu’un objet puisse être employé comme un itérateur, il faut que sa méthode spéciale __next__() soit mise en œuvre. Créons pour exemple un itérateur qui renvoie l’une après l’autre les puissances entières successives d’une base donnée en paramètre du constructeur.

class PowerIt:

    def __init__(self, base):
        self.__base = base
        self.__exponent = 0

    def __next__(self):
        value = self.__base ** self.__exponent
        self.__exponent += 1
        return value


it_2 = PowerIt(2)
it_7 = PowerIt(7)

print("Quelques puissances de 2 :")
for n in range(5):
    print(f"2 ** {n} = {next(it_2)}")

print("Les puissances de 7 inférieures à 25000 :")
n = next(it_7)
while n < 25000:
    print(n)
    n = next(it_7)
Quelques puissances de 2 :
2 ** 0 = 1
2 ** 1 = 2
2 ** 2 = 4
2 ** 3 = 8
2 ** 4 = 16
Les puissances de 7 inférieures à 25000 :
1
7
49
343
2401
16807

Il est ainsi possible d’utiliser la fonction next() (ou directement la méthode __next__()) pour obtenir la valeur suivante de notre itérateur.

4.3.1. Combiner itérateurs et itérables

Il est bien sûr possible et souvent nécessaire d’associer un itérateur personnalisé à un itérable personnalisé. Il faut alors écrire deux classes, l’une pour l’itérateur et l’autre pour l’itérable. L’itérable devra mettre en œuvre la méthode spéciale __iter__() en charge de renvoyer une instance de l’itérateur, qui devra lui-même mettre en œuvre la méthode __next__().

class PowerIt:

    def __init__(self, base, max_exp):
        self.__base = base
        self.__max_exp = max_exp
        self.__exponent = 0

    def __next__(self):
        if self.__exponent == self.__max_exp:
            raise StopIteration
        value = self.__base ** self.__exponent
        self.__exponent += 1
        return value


class Powers:

    def __init__(self, base, max_exp=-1):
        self.__base = base
        self.__max_exp = max_exp

    def __iter__(self):
        return PowerIt(self.__base, self.__max_exp)


p_7 = Powers(7, 6)
p_2 = Powers(2)

print("Les 6 premières puissances de 7 :")
for power in p_7:
    print(power)

print("Les puissances de 2 à la demande (appuyez sur ENTER pour la valeur suivante) :")
for power in p_2:
    input(power)  # affiche la valeur suivante et attend une entrée de l'utilisateur
Les 6 premières puissances de 7 :
1
7
49
343
2401
16807
Les puissances de 2 à la demande (appuyez sur ENTER pour la valeur suivante) :
1
2
4
8
16
...

Cette fois, l’itérateur possède un attribut supplémentaire max_exp qui correspond au nombre maximal d’itérations autorisées. Ce maximum atteint, la méthode spéciale __next__() lève l’exception StopIteration comme le font les itérateurs de listes quand ils en arrivent au bout. Par défaut, nous initialisons l’attribut max_exp à -1, ce qui a pour effet de ne pas imposer de limite à l’itérateur. Celui-ci continuera donc de produire les valeurs à la demande sans jamais lever l’exception StopIteration.

4.3.2. Les itérateurs itérables

Lorsque nous ne nous soucions pas de faire la différence entre itérateur et itérable, il est possible de n’écrire qu’une seule classe faisant office des deux en mettant en œuvre à la fois __iter__() et __next__(), de la manière suivante.

class Powers:

    def __init__(self, base, max_exp=-1):
        self.__base = base
        self.__max_exp = max_exp
        self.__exponent = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.__exponent == self.__max_exp:
            raise StopIteration
        value = self.__base ** self.__exponent
        self.__exponent += 1
        return value

L’utilisation de la classe et son fonctionnement sont identiques à la version précédente de notre programme. La méthode __iter__() de la classe Powers retourne ici l’instance courante même, car c’est celle-ci qui sert aussi d’itérateur.