Itérateurs fantastiques et comment les faire

Photo par John Matychuk sur Unsplash

Le problème

Pendant l’apprentissage à Make School, j’ai vu mes pairs écrire des fonctions qui créent des listes d’éléments.

s = 'baacabcaab'
p = 'a'
def find_char (chaîne, caractère):
  indices = liste ()
  pour index, str_char dans énumérer (chaîne):
    si str_char == caractère:
      indices.append (index)
  renvoyer des indices
print (find_char (s, p)) # [1, 2, 4, 7, 8]

Cette implémentation fonctionne, mais elle pose quelques problèmes:

  • Et si nous ne voulions que le premier résultat; aurons-nous besoin de créer une fonction entièrement nouvelle?
  • Et si tout ce que nous faisions est une fois le résultat en boucle, devons-nous stocker chaque élément en mémoire?

Les itérateurs sont la solution idéale à ces problèmes. Ils fonctionnent comme des "listes paresseuses" en ce sens qu'au lieu de renvoyer une liste avec chaque valeur produite, ils renvoient chaque élément un à un.

Les itérateurs retournent paresseusement des valeurs; économiser de la mémoire.

Alors, plongeons-nous dans leur apprentissage!

Itérateurs intégrés

Les itérateurs les plus souvent sont énumération () et zip (). Tous les deux retournent des valeurs par next () avec eux.

range (), cependant, n'est pas un itérateur, mais un «itératif paresseux». - Explication

Nous pouvons convertir range () en un itérateur avec iter (), nous le ferons donc pour nos exemples dans un but d’apprentissage.

my_iter = iter (range (10))
print (next (my_iter)) # 0
print (next (my_iter)) # 1

À chaque appel de next (), nous obtenons la valeur suivante dans notre plage; est logique, non? Si vous voulez convertir un itérateur en liste, il vous suffit de lui donner le constructeur de liste.

my_iter = iter (range (10))
print (list (my_iter)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Si nous imitons ce comportement, nous comprendrons mieux le fonctionnement des itérateurs.

my_iter = iter (range (10))
my_list = list ()
essayer:
  alors que vrai:
    my_list.append (next (my_iter))
sauf StopIteration:
  passer
print (my_list) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Vous pouvez voir que nous devions envelopper dans une déclaration try catch. C’est parce que les itérateurs lèvent StopIteration quand ils sont épuisés.

Donc, si nous appelons ensuite notre itérateur épuisé, nous aurons cette erreur.

next (my_iter) # Lève: StopIteration

Faire un itérateur

Essayons de créer un itérateur qui se comporte comme une plage avec seulement l’argument stop en utilisant trois types d’itérateurs courants: les classes, les fonctions de générateur (rendement) et les expressions de générateur.

Classe

L'ancienne façon de créer un itérateur consistait à définir explicitement une classe. Pour qu'un objet soit un itérateur, il doit implémenter __iter __ () qui se retourne lui-même et __next __ () qui renvoie la valeur suivante.

classe my_range:
  _current = -1
  def __init __ (self, stop):
    self._stop = stop
  def __iter __ (auto):
    retourner soi-même
  def __next __ (self):
    self._current + = 1
    si self._current> = self._stop:
      lever StopIteration
    retour de self._current
r = my_range (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Ce n’était pas trop difficile, mais malheureusement, nous devons garder une trace des variables entre les appels de next (). Personnellement, je n’aime pas le passe-partout ou la modification de ma façon de penser les boucles, car ce n’est pas une solution de remplacement, alors je préfère les générateurs.

Le principal avantage est que nous pouvons ajouter des fonctions supplémentaires qui modifient ses variables internes telles que _stop ou créent de nouveaux itérateurs.

Les itérateurs de classe ont l'inconvénient d'avoir besoin d'un passe-partout, toutefois, ils peuvent avoir des fonctions supplémentaires qui modifient l'état.

Générateurs

Le PEP 255 a introduit les «générateurs simples» à l’aide du mot-clé rendement.

Aujourd'hui, les générateurs sont des itérateurs plus faciles à réaliser que leurs homologues de classe.

Fonction de générateur

Les fonctions de générateur sont ce qui a finalement été discuté dans ce PEP et sont mon type d’itérateur préféré, alors commençons par cela.

def my_range (stop):
  indice = 0
  tant que l'index 
r = my_range (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Voyez-vous à quel point ces 4 lignes de code sont belles? Pour couronner le tout, c’est un peu plus court que la mise en œuvre de notre liste!

Les fonctions de générateur utilisent des itérateurs avec un nombre moins élevé que les classes avec un flux logique normal.

Le générateur fonctionne automatiquement "en pause" et renvoie la valeur spécifiée à chaque appel de next (). Cela signifie qu'aucun code n'est exécuté avant le premier appel de next ().

Cela signifie que le flux est comme ça:

  1. next () est appelé,
  2. Le code est exécuté jusqu'à la prochaine déclaration de rendement.
  3. La valeur sur le droit de rendement est renvoyée.
  4. L'exécution est en pause.
  5. 1 à 5 répéter pour chaque appel suivant () jusqu'à ce que la dernière ligne de code soit frappée.
  6. StopIteration est levé.

Les fonctions de générateur vous permettent également d’utiliser le rendement du mot clé qui appelle les futurs appels next () à un autre itérable jusqu’à épuisement de ce dernier.

def yielded_range ():
  rendement de my_range (10)
print (list (yielded_range ())) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Ce n’était pas un exemple particulièrement complexe. Mais vous pouvez même le faire récursivement!

def my_range_recursive (stop, current = 0):
  si courant> = stop:
    revenir
  courant de rendement
  rendement de my_range_recursive (stop, current + 1)
r = my_range_recursive (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Expression du générateur

Les expressions de générateur nous permettent de créer des itérateurs en une ligne et sont utiles lorsque nous n’avons pas besoin de lui attribuer des fonctions externes. Malheureusement, nous ne pouvons pas créer d’autres my_range en utilisant une expression, mais nous pouvons travailler sur des iterables comme notre dernière fonction my_range.

my_doubled_range_10 = (x * 2 pour x dans my_range (10))
print (list (my_doubled_range_10)) # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

La chose intéressante à propos de cela est qu'il fait ce qui suit:

  1. La liste demande à my_doubled_range_10 sa prochaine valeur.
  2. my_doubled_range_10 demande à my_range sa prochaine valeur.
  3. my_doubled_range_10 renvoie la valeur de my_range multipliée par 2.
  4. La liste ajoute la valeur à elle-même.
  5. 1 à 5 répéter jusqu'à ce que my_doubled_range_10 lève StopIteration, ce qui se produit lorsque my_range le fait.
  6. La liste est renvoyée avec chaque valeur renvoyée par my_doubled_range.

Nous pouvons même faire du filtrage en utilisant des expressions de générateur!

my_even_range_10 = (x pour x dans my_range (10) si x% 2 == 0)
print (list (my_even_range_10)) # [0, 2, 4, 6, 8]

Ceci est très similaire à la précédente, excepté que my_even_range_10 ne renvoie que les valeurs qui correspondent à la condition donnée, donc seules les valeurs comprises dans la plage [0, 10).

Pendant tout ce temps, nous ne créons une liste que parce que nous l’avons dit.

Le bénéfice

La source

Parce que les générateurs sont des itérateurs, les itérateurs sont des itérables, et les itérateurs renvoient des valeurs paresseux. Cela signifie qu'en utilisant cette connaissance, nous pouvons créer des objets qui ne nous donneront des objets que lorsque nous les demanderons et quel que soit notre choix.

Cela signifie que nous pouvons passer des générateurs à des fonctions qui se réduisent mutuellement.

print (sum (my_range (10))) # 45

Le calcul de la somme de cette manière évite de créer une liste lorsque nous ne faisons qu’ajouter, puis de la supprimer.

Nous pouvons réécrire le tout premier exemple pour être bien meilleur en utilisant une fonction de générateur!

s = 'baacabcaab'
p = 'a'
def find_char (chaîne, caractère):
  pour index, str_char dans énumérer (chaîne):
    si str_char == caractère:
      indice de rendement
print (list (find_char (s, p))) # [1, 2, 4, 7, 8]

Maintenant, tout de suite, il n’ya aucun avantage évident, mais passons à ma première question: «Et si nous ne voulions que le premier résultat; aurons-nous besoin de créer une fonction entièrement nouvelle?

Avec une fonction de générateur, nous n’avons pas besoin de réécrire autant de logique.
print (next (find_char (s, p))) # 1

Nous pouvons maintenant récupérer la première valeur de la liste fournie par notre solution d'origine, mais de cette manière, nous obtenons uniquement la première correspondance et nous arrêtons de parcourir la liste. Le générateur sera alors jeté et rien d’autre n’est créé; massivement économiser de la mémoire.

Conclusion

Si vous créez une fonction, elle accumule des valeurs dans une liste comme celle-ci.

def foo (bar):
  valeurs = []
  pour x en bar:
    # un peu de logique
    values.append (x)
  valeurs de retour

Pensez-y à renvoyer un itérateur avec une classe, une fonction génératrice ou une expression génératrice comme ceci:

def foo (bar):
  pour x en bar:
    # un peu de logique
    rendement x

Ressources et sources

PEP

  • Générateurs
  • Générateur Expressions PEP
  • Rendement De PEP

Articles et discussions

  • Itérateurs
  • Itérable vs Itérateur
  • Documentation du générateur
  • Itérateurs vs générateurs
  • Générateur Expression vs Fonction
  • Générateurs Recrutifs

Définitions

  • Iterable
  • Itérateur
  • Générateur
  • Générateur Itérateur
  • Expression du générateur

Publié à l'origine à l'adresse https://blog.dacio.dev/2019/05/03/python-iterators-and-generators/.