Personnaliser l'itération avec des générateurs

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Dans ce laboratoire, vous apprendrez à personnaliser les itérations à l'aide de générateurs en Python. Vous implémenterez également la fonctionnalité d'itérateur dans des classes personnalisées et créerez des générateurs pour les sources de données en flux.

Le fichier structure.py sera modifié, et un nouveau fichier nommé follow.py sera créé au cours de l'expérience.

Comprendre les générateurs Python

Les générateurs sont une fonctionnalité puissante en Python. Ils offrent un moyen simple et élégant de créer des itérateurs. En Python, lorsque vous manipulez des séquences de données, les itérateurs sont très utiles car ils vous permettent de parcourir une série de valeurs une par une. Les fonctions ordinaires retournent généralement une seule valeur puis cessent d'exécuter. Cependant, les générateurs sont différents. Ils peuvent produire une séquence de valeurs au fil du temps, ce qui signifie qu'ils peuvent générer plusieurs valeurs de manière progressive.

Qu'est - ce qu'un générateur ?

Une fonction générateur ressemble à une fonction ordinaire. Mais la différence clé réside dans la façon dont elle retourne des valeurs. Au lieu d'utiliser l'instruction return pour fournir un seul résultat, une fonction générateur utilise l'instruction yield. L'instruction yield est spéciale. Chaque fois qu'elle est exécutée, l'état de la fonction est mis en pause, et la valeur qui suit le mot - clé yield est retournée à l'appelant. Lorsque la fonction générateur est appelée à nouveau, elle reprend son exécution là où elle s'était arrêtée.

Commençons par créer une simple fonction générateur. La fonction intégrée range() en Python ne prend pas en charge les pas fractionnaires. Nous allons donc créer une fonction générateur qui peut produire une plage de nombres avec un pas fractionnaire.

  1. Tout d'abord, vous devez ouvrir un nouveau terminal Python dans le WebIDE. Pour ce faire, cliquez sur le menu "Terminal", puis sélectionnez "New Terminal".
  2. Une fois le terminal ouvert, tapez le code suivant dans le terminal. Ce code définit une fonction générateur puis la teste.
def frange(start, stop, step):
    current = start
    while current < stop:
        yield current
        current += step

## Test the generator with a for loop
for x in frange(0, 2, 0.25):
    print(x, end=' ')

Dans ce code, la fonction frange est une fonction générateur. Elle initialise une variable current avec la valeur start. Ensuite, tant que current est inférieur à la valeur stop, elle produit la valeur current puis incrémente current de la valeur step. La boucle for parcourt ensuite les valeurs produites par la fonction générateur frange et les affiche.

Vous devriez voir la sortie suivante :

0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

La nature à usage unique des générateurs

Une caractéristique importante des générateurs est qu'ils sont épuisables. Cela signifie qu'une fois que vous avez parcouru toutes les valeurs produites par un générateur, il ne peut plus être utilisé pour produire la même séquence de valeurs. Illustrons cela avec le code suivant :

## Create a generator object
f = frange(0, 2, 0.25)

## First iteration works fine
print("First iteration:")
for x in f:
    print(x, end=' ')
print("\n")

## Second iteration produces nothing
print("Second iteration:")
for x in f:
    print(x, end=' ')
print("\n")

Dans ce code, nous créons tout d'abord un objet générateur f en utilisant la fonction frange. La première boucle for parcourt toutes les valeurs produites par le générateur et les affiche. Après la première itération, le générateur a été épuisé, ce qui signifie qu'il a déjà produit toutes les valeurs qu'il peut. Ainsi, lorsque nous essayons de le parcourir à nouveau dans la deuxième boucle for, il ne produit aucune nouvelle valeur.

Sortie :

First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Second iteration:

Notez que la deuxième itération n'a produit aucune sortie car le générateur était déjà épuisé.

Création de générateurs réutilisables avec des classes

Si vous avez besoin de parcourir plusieurs fois la même séquence de valeurs, vous pouvez encapsuler le générateur dans une classe. En faisant cela, chaque fois que vous commencez une nouvelle itération, un nouveau générateur sera créé.

class FRange:
    def __init__(self, start, stop, step):
        self.start = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        n = self.start
        while n < self.stop:
            yield n
            n += self.step

## Create an instance
f = FRange(0, 2, 0.25)

## We can iterate multiple times
print("First iteration:")
for x in f:
    print(x, end=' ')
print("\n")

print("Second iteration:")
for x in f:
    print(x, end=' ')
print("\n")

Dans ce code, nous définissons une classe FRange. La méthode __init__ initialise les valeurs start, stop et step. La méthode __iter__ est une méthode spéciale dans les classes Python. Elle est utilisée pour créer un itérateur. À l'intérieur de la méthode __iter__, nous avons un générateur qui produit des valeurs de la même manière que la fonction frange que nous avons définie précédemment.

Lorsque nous créons une instance f de la classe FRange et que nous la parcourons plusieurs fois, chaque itération appelle la méthode __iter__, qui crée un nouveau générateur. Ainsi, nous pouvons obtenir la même séquence de valeurs plusieurs fois.

Sortie :

First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Second iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Cette fois - ci, nous pouvons parcourir plusieurs fois car la méthode __iter__() crée un nouveau générateur chaque fois qu'elle est appelée.

Ajout d'itérations à des classes personnalisées

Maintenant que vous avez compris les bases des générateurs, nous allons les utiliser pour ajouter des capacités d'itération à des classes personnalisées. En Python, si vous souhaitez rendre une classe itérable, vous devez implémenter la méthode spéciale __iter__(). Une classe itérable vous permet de parcourir ses éléments, tout comme vous pouvez parcourir une liste ou un tuple. C'est une fonctionnalité puissante qui rend vos classes personnalisées plus flexibles et plus faciles à utiliser.

Comprendre la méthode __iter__()

La méthode __iter__() est une partie cruciale pour rendre une classe itérable. Elle doit retourner un objet itérateur. Un itérateur est un objet sur lequel on peut itérer (parcourir en boucle). Une façon simple et efficace d'y parvenir est de définir __iter__() comme une fonction générateur. Une fonction générateur utilise le mot - clé yield pour produire une séquence de valeurs une à une. Chaque fois que l'instruction yield est rencontrée, la fonction se met en pause et retourne la valeur. La prochaine fois que l'itérateur est appelé, la fonction reprend là où elle s'était arrêtée.

Modification de la classe Structure

Dans la configuration de ce laboratoire, nous avons fourni une classe de base Structure. D'autres classes, comme Stock, peuvent hériter de cette classe Structure. L'héritage est un moyen de créer une nouvelle classe qui hérite des propriétés et des méthodes d'une classe existante. En ajoutant une méthode __iter__() à la classe Structure, nous pouvons rendre toutes ses sous - classes itérables. Cela signifie que toute classe qui hérite de Structure aura automatiquement la capacité d'être parcourue en boucle.

  1. Ouvrez le fichier structure.py dans le WebIDE :
cd ~/project

Cette commande change le répertoire de travail actuel pour le répertoire project où se trouve le fichier structure.py. Vous devez être dans le bon répertoire pour accéder et modifier le fichier.

  1. Examinez l'implémentation actuelle de la classe Structure :
class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

La classe Structure a une liste _fields qui stocke les noms des attributs. La méthode __init__() est le constructeur de la classe. Elle initialise les attributs de l'objet en vérifiant si le nombre d'arguments passés est égal au nombre de champs. Sinon, elle lève une erreur TypeError. Sinon, elle définit les attributs à l'aide de la fonction setattr().

  1. Ajoutez une méthode __iter__() qui produit chaque valeur d'attribut dans l'ordre :
def __iter__(self):
    for name in self._fields:
        yield getattr(self, name)

Cette méthode __iter__() est une fonction générateur. Elle parcourt la liste _fields et utilise la fonction getattr() pour obtenir la valeur de chaque attribut. Le mot - clé yield retourne ensuite la valeur une par une.

Le fichier structure.py complet devrait maintenant ressembler à ceci :

class StructureMeta(type):
    def __new__(cls, name, bases, clsdict):
        fields = clsdict.get('_fields', [])
        for name in fields:
            clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
        return super().__new__(cls, name, bases, clsdict)

class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

    def __iter__(self):
        for name in self._fields:
            yield getattr(self, name)

Cette classe Structure mise à jour dispose maintenant de la méthode __iter__(), qui la rend et ses sous - classes itérables.

  1. Enregistrez le fichier. Après avoir apporté des modifications au fichier structure.py, vous devez l'enregistrer pour que les modifications soient appliquées.

  2. Testons maintenant la capacité d'itération en créant une instance de Stock et en la parcourant en boucle :

python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('Iterating over Stock:'); [print(val) for val in s]"

Cette commande crée une instance de la classe Stock, qui hérite de la classe Structure. Elle parcourt ensuite l'instance à l'aide d'une compréhension de liste et affiche chaque valeur.

Vous devriez voir une sortie comme celle - ci :

Iterating over Stock:
GOOG
100
490.1

Maintenant, toute classe qui hérite de Structure sera automatiquement itérable, et l'itération produira les valeurs d'attribut dans l'ordre défini par la liste _fields. Cela signifie que vous pouvez facilement parcourir les attributs de n'importe quelle sous - classe de Structure sans avoir à écrire de code supplémentaire pour l'itération.

Amélioration des classes avec des capacités d'itération

Maintenant, nous avons rendu notre classe Structure et ses sous - classes compatibles avec l'itération. L'itération est un concept puissant en Python qui vous permet de parcourir une collection d'éléments un par un. Lorsqu'une classe prend en charge l'itération, elle devient plus flexible et peut fonctionner avec de nombreuses fonctionnalités intégrées de Python. Explorons comment cette prise en charge de l'itération permet d'utiliser de nombreuses fonctionnalités puissantes en Python.

Exploitation de l'itération pour les conversions de séquences

En Python, il existe des fonctions intégrées telles que list() et tuple(). Ces fonctions sont très utiles car elles peuvent prendre n'importe quel objet itérable en entrée. Un objet itérable est quelque chose sur lequel vous pouvez itérer, comme une liste, un tuple, ou maintenant, les instances de notre classe Structure. Étant donné que notre classe Structure prend maintenant en charge l'itération, nous pouvons facilement convertir ses instances en listes ou en tuples.

  1. Essayons ces opérations avec une instance de Stock. La classe Stock est une sous - classe de Structure. Exécutez la commande suivante dans votre terminal :
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('As list:', list(s)); print('As tuple:', tuple(s))"

Cette commande importe d'abord la classe Stock, crée une instance de celle - ci, puis convertit cette instance en une liste et en un tuple à l'aide des fonctions list() et tuple() respectivement. La sortie vous montrera l'instance représentée sous forme de liste et de tuple :

As list: ['GOOG', 100, 490.1]
As tuple: ('GOOG', 100, 490.1)

Désempilement (Unpacking)

Python a une fonctionnalité très utile appelée désempilement (unpacking). Le désempilement vous permet de prendre un objet itérable et d'affecter ses éléments à des variables individuelles en une seule opération. Étant donné que notre instance de Stock est itérable, nous pouvons utiliser cette fonctionnalité de désempilement sur elle.

python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); name, shares, price = s; print(f'Name: {name}, Shares: {shares}, Price: {price}')"

Dans ce code, nous créons une instance de Stock puis nous désempilons ses éléments dans trois variables : name, shares et price. Ensuite, nous affichons ces variables. La sortie montrera les valeurs de ces variables :

Name: GOOG, Shares: 100, Price: 490.1

Ajout de capacités de comparaison

Lorsqu'une classe prend en charge l'itération, il devient plus facile d'implémenter des opérations de comparaison. Les opérations de comparaison sont utilisées pour vérifier si deux objets sont égaux ou non. Ajoutons une méthode __eq__() à notre classe Structure pour comparer les instances.

  1. Ouvrez à nouveau le fichier structure.py. La méthode __eq__() est une méthode spéciale en Python qui est appelée lorsque vous utilisez l'opérateur == pour comparer deux objets. Ajoutez le code suivant à la classe Structure dans le fichier structure.py :
def __eq__(self, other):
    return isinstance(other, type(self)) and tuple(self) == tuple(other)

Cette méthode vérifie d'abord si l'objet other est une instance de la même classe que self à l'aide de la fonction isinstance(). Ensuite, elle convertit à la fois self et other en tuples et vérifie si ces tuples sont égaux.

Le fichier structure.py complet devrait maintenant ressembler à ceci :

class StructureMeta(type):
    def __new__(cls, name, bases, clsdict):
        fields = clsdict.get('_fields', [])
        for name in fields:
            clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
        return super().__new__(cls, name, bases, clsdict)

class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

    def __iter__(self):
        for name in self._fields:
            yield getattr(self, name)

    def __eq__(self, other):
        return isinstance(other, type(self)) and tuple(self) == tuple(other)
  1. Après avoir ajouté la méthode __eq__(), enregistrez le fichier structure.py.

  2. Testons la capacité de comparaison. Exécutez la commande suivante dans votre terminal :

python3 -c "from stock import Stock; a = Stock('GOOG', 100, 490.1); b = Stock('GOOG', 100, 490.1); c = Stock('AAPL', 200, 123.4); print(f'a == b: {a == b}'); print(f'a == c: {a == c}')"

Ce code crée trois instances de Stock : a, b et c. Ensuite, il compare a avec b et a avec c à l'aide de l'opérateur ==. La sortie montrera les résultats de ces comparaisons :

a == b: True
a == c: False
  1. Maintenant, pour nous assurer que tout fonctionne correctement, nous devons exécuter les tests unitaires. Les tests unitaires sont un ensemble de code qui vérifie si différentes parties de votre programme fonctionnent comme prévu. Exécutez la commande suivante dans votre terminal :
python3 teststock.py

Si tout fonctionne correctement, vous devriez voir une sortie indiquant que les tests ont réussi :

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

En ajoutant seulement deux méthodes simples (__iter__() et __eq__()), nous avons considérablement amélioré notre classe Structure avec des capacités qui la rendent plus "Pythonique" et plus facile à utiliser.

Création d'un générateur pour les données en flux

En programmation, les générateurs sont un outil puissant, surtout lorsqu'il s'agit de résoudre des problèmes réels tels que la surveillance d'une source de données en flux. Dans cette section, nous allons apprendre à appliquer ce que nous avons appris sur les générateurs à un tel scénario pratique. Nous allons créer un générateur qui surveille un fichier journal et nous fournit les nouvelles lignes au fur et à mesure qu'elles sont ajoutées au fichier.

Configuration de la source de données

Avant de commencer à créer le générateur, nous devons configurer une source de données. Dans ce cas, nous allons utiliser un programme de simulation qui génère des données sur le marché boursier.

Tout d'abord, vous devez ouvrir un nouveau terminal dans le WebIDE. C'est là que vous exécuterez les commandes pour démarrer la simulation.

Après avoir ouvert le terminal, vous exécuterez le programme de simulation boursière. Voici les commandes que vous devez entrer :

cd ~/project
python3 stocksim.py

La première commande cd ~/project change le répertoire actuel pour le répertoire project dans votre répertoire personnel. La deuxième commande python3 stocksim.py exécute le programme de simulation boursière. Ce programme générera des données sur le marché boursier et les écrira dans un fichier nommé stocklog.csv dans le répertoire actuel. Laissez ce programme s'exécuter en arrière - plan tandis que nous travaillons sur le code de surveillance.

Création d'un simple moniteur de fichier

Maintenant que nous avons configuré notre source de données, créons un programme qui surveille le fichier stocklog.csv. Ce programme affichera tout changement de prix négatif.

  1. Tout d'abord, créez un nouveau fichier appelé follow.py dans le WebIDE. Pour ce faire, vous devez changer le répertoire pour le répertoire project en utilisant la commande suivante dans le terminal :
cd ~/project
  1. Ensuite, ajoutez le code suivant au fichier follow.py. Ce code ouvre le fichier stocklog.csv, déplace le pointeur de fichier à la fin du fichier, puis vérifie en continu s'il y a de nouvelles lignes. Si une nouvelle ligne est trouvée et qu'elle représente un changement de prix négatif, il affiche le nom de l'action, le prix et le changement.
## follow.py
import os
import time

f = open('stocklog.csv')
f.seek(0, os.SEEK_END)   ## Move file pointer 0 bytes from end of file

while True:
    line = f.readline()
    if line == '':
        time.sleep(0.1)   ## Sleep briefly and retry
        continue
    fields = line.split(',')
    name = fields[0].strip('"')
    price = float(fields[1])
    change = float(fields[4])
    if change < 0:
        print('%10s %10.2f %10.2f' % (name, price, change))
  1. Après avoir ajouté le code, enregistrez le fichier. Ensuite, exécutez le programme en utilisant la commande suivante dans le terminal :
python3 follow.py

Vous devriez voir une sortie qui montre les actions avec des changements de prix négatifs. Cela pourrait ressembler à ceci :

      AAPL     148.24      -1.76
      GOOG    2498.45      -1.55

Si vous souhaitez arrêter le programme, appuyez sur Ctrl+C dans le terminal.

Conversion en fonction générateur

Bien que le code précédent fonctionne, nous pouvons le rendre plus réutilisable et modulaire en le convertissant en une fonction générateur. Une fonction générateur est un type spécial de fonction qui peut être mise en pause et reprise, et qui produit des valeurs une par une.

  1. Ouvrez à nouveau le fichier follow.py et modifiez - le pour utiliser une fonction générateur. Voici le code mis à jour :
## follow.py
import os
import time

def follow(filename):
    """
    Generator function that yields new lines in a file as they are added.
    Similar to the 'tail -f' Unix command.
    """
    f = open(filename)
    f.seek(0, os.SEEK_END)   ## Move to the end of the file

    while True:
        line = f.readline()
        if line == '':
            time.sleep(0.1)   ## Sleep briefly and retry
            continue
        yield line

## Example usage - monitor stocks with negative price changes
if __name__ == '__main__':
    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if change < 0:
            print('%10s %10.2f %10.2f' % (name, price, change))

La fonction follow est maintenant une fonction générateur. Elle ouvre le fichier, se déplace à la fin, puis vérifie en continu s'il y a de nouvelles lignes. Lorsqu'une nouvelle ligne est trouvée, elle la produit.

  1. Enregistrez le fichier et exécutez - le à nouveau en utilisant la commande :
python3 follow.py

La sortie devrait être la même que précédemment. Mais maintenant, la logique de surveillance de fichier est soigneusement encapsulée dans la fonction générateur follow. Cela signifie que nous pouvons réutiliser cette fonction dans d'autres programmes qui ont besoin de surveiller un fichier.

Comprendre la puissance des générateurs

En convertissant notre code de lecture de fichier en une fonction générateur, nous l'avons rendu beaucoup plus flexible et réutilisable. La fonction follow() peut être utilisée dans n'importe quel programme qui a besoin de surveiller un fichier, pas seulement pour les données boursières.

Par exemple, vous pourriez l'utiliser pour surveiller les journaux de serveur, les journaux d'application ou tout autre fichier qui est mis à jour au fil du temps. Cela montre comment les générateurs sont un excellent moyen de gérer les sources de données en flux de manière propre et modulaire.

Résumé

Dans ce laboratoire (lab), vous avez appris à personnaliser l'itération en Python en utilisant les générateurs. Vous avez créé de simples générateurs avec l'instruction yield pour générer des séquences de valeurs, ajouté la prise en charge de l'itération à des classes personnalisées en implémentant la méthode __iter__(), exploité l'itération pour les conversions de séquences, le désempilement (unpacking) et la comparaison, et construit un générateur pratique pour surveiller une source de données en flux.

Les générateurs sont une fonctionnalité puissante de Python qui vous permet de créer des itérateurs avec un code minimal. Ils sont particulièrement utiles pour le traitement de grands ensembles de données, le travail avec des données en flux, la création de pipelines de données et la mise en œuvre de motifs d'itération personnalisés. L'utilisation de générateurs vous permet d'écrire un code plus propre et plus efficace en termes de mémoire qui exprime clairement votre intention.