Python - Itérateurs

Bonjour, aspirants programmeurs Python ! Aujourd'hui, nous allons entreprendre un voyage passionnant dans le monde des itérateurs Python. En tant que votre enseignant de science informatique de quartier, je suis ravi de vous guider à travers ce sujet fascinant. Alors, prenez votre boisson préférée, mettez-vous à l'aise, et plongeons-y !

Python - Iterators

Python Itérateurs

Qu'est-ce qu'un itérateur ?

Imaginez que vous avez une grande boîte de briques Lego colorées. Un itérateur est comme une main magique qui peut rentrer dans la boîte et sortir une brique Lego à la fois, vous permettant d'examiner chaque brique individuellement sans déverser tout le contenu de la boîte par terre. En Python, les itérateurs fonctionnent de manière similaire, nous permettant de travailler avec des collections de données un élément à la fois.

Comment fonctionnent les itérateurs ?

Les itérateurs en Python sont des objets qui implémentent deux méthodes spéciales : __iter__() et __next__(). Ne vous inquiétez pas si cela vous paraît un peu ésotérique pour le moment – nous allons le décomposer étape par étape !

  1. La méthode __iter__() retourne l'objet itérateur lui-même. C'est comme dire, "Eh, je suis prêt à commencer à distribuer des briques Lego !"
  2. La méthode __next__() retourne l'élément suivant de la séquence. C'est comme rentrer dans la boîte et sortir la prochaine brique Lego.

Voyons cela en action avec un exemple simple :

# Création d'une liste (notre boîte de briques Lego)
ma_liste = [1, 2, 3, 4, 5]

# Obtenir un itérateur de la liste
mon_iterateur = iter(ma_liste)

# Utiliser next() pour obtenir des éléments un par un
print(next(mon_iterateur))  # Sortie : 1
print(next(mon_iterateur))  # Sortie : 2
print(next(mon_iterateur))  # Sortie : 3

Dans cet exemple, iter(ma_liste) crée un objet itérateur pour notre liste. Ensuite, chaque appel à next(mon_iterateur) récupère l'élément suivant de la liste.

Le pouvoir des itérateurs dans les boucles

Voici un fait amusant : lorsque vous utilisez une boucle for en Python, vous utilisez en réalité un itérateur en arrière-plan ! Voyons comment :

ma_liste = ["pomme", "banane", "cerise"]

for fruit in ma_liste:
print(f"J'aime {fruit} !")

# Sortie :
# J'aime pomme !
# J'aime banane !
# J'aime cerise !

Python crée automatiquement un itérateur de ma_liste et utilise __next__() pour obtenir chaque élément pour la boucle. C'est pas génial ?

Gestion des erreurs dans les itérateurs

Maintenant, que se passe-t-il lorsque notre main magique atteint une boîte vide ? En termes Python, qu'arrive-t-il lorsque il ne reste plus d'éléments dans l'itérateur ? C'est là que la gestion des erreurs entre en jeu.

Lorsqu'un itérateur est épuisé (plus d'éléments), il lève une exception StopIteration. Voyons cela en action :

ma_liste = [1, 2, 3]
mon_iterateur = iter(ma_liste)

print(next(mon_iterateur))  # Sortie : 1
print(next(mon_iterateur))  # Sortie : 2
print(next(mon_iterateur))  # Sortie : 3
print(next(mon_iterateur))  # Lève une exception StopIteration

Pour gérer cela en douceur, nous pouvons utiliser un bloc try-except :

ma_liste = [1, 2, 3]
mon_iterateur = iter(ma_liste)

try:
while True:
item = next(mon_iterateur)
print(item)
except StopIteration:
print("Fin de l'itérateur atteinte !")

# Sortie :
# 1
# 2
# 3
# Fin de l'itérateur atteinte !

De cette manière, nous pouvons traiter tous les éléments et gérer la fin de l'itérateur en douceur.

Itérateur personnalisé

Maintenant que nous comprenons comment fonctionnent les itérateurs, créons le nôtre ! Imaginez que nous voulons créer un itérateur de décompte. Voici comment nous pourrions le faire :

class Decompte:
def __init__(self, depart):
self.depart = depart

def __iter__(self):
return self

def __next__(self):
if self.depart <= 0:
raise StopIteration
self.depart -= 1
return self.depart + 1

# Utilisation de notre itérateur personnalisé
decompte = Decompte(5)
for nombre in decompte:
print(nombre)

# Sortie :
# 5
# 4
# 3
# 2
# 1

Dans cet exemple, nous avons créé une classe Decompte qui agit à la fois comme un itérable (elle a une méthode __iter__() ) et un itérateur (elle a une méthode __next__() ). Chaque fois que __next__() est appelé, il retourne le nombre suivant dans la séquence de décompte.

Itérateur asynchrone

À mesure que nous entrons dans des terres plus avancées, abordons brièvement les itérateurs asynchrones. Ceux-ci sont utilisés dans la programmation asynchrone, qui est une manière d'écrire du code concurrent.

Un itérateur asynchrone est similaire à un itérateur régulier, mais il utilise les mots-clés async et await. Voici un exemple simple :

import asyncio

class AsyncDecompte:
def __init__(self, depart):
self.depart = depart

def __aiter__(self):
return self

async def __anext__(self):
await asyncio.sleep(1)  # Simuler une opération asynchrone
if self.depart <= 0:
raise StopAsyncIteration
self.depart -= 1
return self.depart + 1

async def main():
async for nombre in AsyncDecompte(5):
print(nombre)

asyncio.run(main())

# Sortie (avec des délais de 1 seconde) :
# 5
# 4
# 3
# 2
# 1

Cet itérateur asynchrone fonctionne de manière similaire à notre classe Decompte précédente, mais il permet des opérations asynchrones (simulées ici avec asyncio.sleep(1)).

Tableau des méthodes des itérateurs

Voici un tableau pratique résumant les méthodes clés que nous avons discutées :

Méthode Description Utilisé dans
__iter__() Retourne l'objet itérateur Itérateurs réguliers
__next__() Retourne l'élément suivant de la séquence Itérateurs réguliers
__aiter__() Retourne l'objet itérateur asynchrone Itérateurs asynchrones
__anext__() Retourne l'élément suivant de la séquence asynchrone Itérateurs asynchrones

Et voilà, mes amis ! Nous avons parcouru la terre des itérateurs Python, des bases à la création de notre propre itérateur, en passant par les itérateurs asynchrones. N'oubliez pas, comme apprendre à construire avec des Lego, maîtriser les itérateurs prend de la pratique. Alors, n'hésitez pas à expérimenter et à créer vos propres itérateurs. Bon codage, et que l'itérateur soit avec vous !

Credits: Image by storyset