Gestion des instructions yield en Python

Beginner

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

Introduction

Dans ce laboratoire, vous apprendrez à gérer ce qui se passe aux instructions yield en Python. Vous comprendrez comment gérer efficacement les opérations et les comportements associés à ces instructions.

De plus, vous apprendrez à propos de la durée de vie des générateurs et de la gestion des exceptions dans les générateurs. Les fichiers follow.py et cofollow.py seront modifiés au cours de ce processus d'apprentissage.

Comprendre la durée de vie et la fermeture des générateurs

Dans cette étape, nous allons explorer la durée de vie des générateurs Python et apprendre à les fermer correctement. Les générateurs en Python sont un type spécial d'itérateur qui vous permet de générer une séquence de valeurs à la volée, plutôt que de les calculer toutes d'un coup et de les stocker en mémoire. Cela peut être très utile lorsqu'il s'agit de traiter de grands ensembles de données ou des séquences infinies.

Qu'est-ce que le générateur follow() ?

Commençons par regarder le fichier follow.py dans le répertoire du projet. Ce fichier contient une fonction générateur appelée follow(). Une fonction générateur est définie comme une fonction normale, mais au lieu d'utiliser le mot-clé return, elle utilise yield. Lorsqu'une fonction générateur est appelée, elle retourne un objet générateur, sur lequel vous pouvez itérer pour obtenir les valeurs qu'elle produit.

La fonction générateur follow() lit en continu les lignes d'un fichier et produit chaque ligne au fur et à mesure qu'elle est lue. Cela est similaire à la commande Unix tail -f, qui surveille en continu un fichier pour les nouvelles lignes.

Ouvrez le fichier follow.py dans l'éditeur WebIDE :

import os
import time

def follow(filename):
    with open(filename,'r') as f:
        f.seek(0,os.SEEK_END)
        while True:
            line = f.readline()
            if line == '':
                time.sleep(0.1)    ## Sleep briefly to avoid busy wait
                continue
            yield line

Dans ce code, l'instruction with open(filename, 'r') as f ouvre le fichier en mode lecture et s'assure qu'il est correctement fermé lorsque le bloc est quitté. La ligne f.seek(0, os.SEEK_END) déplace le pointeur de fichier à la fin du fichier, de sorte que le générateur commence à lire à partir de la fin. La boucle while True lit en continu les lignes du fichier. Si la ligne est vide, cela signifie qu'il n'y a pas encore de nouvelles lignes, donc le programme dort pendant 0,1 seconde pour éviter une attente active puis passe à l'itération suivante. Si la ligne n'est pas vide, elle est produite.

Ce générateur s'exécute dans une boucle infinie, ce qui soulève une question importante : que se passe-t-il lorsque nous arrêtons d'utiliser le générateur ou que nous voulons le terminer avant la fin ?

Modifier le générateur pour gérer la fermeture

Nous devons modifier la fonction follow() dans follow.py pour gérer le cas où le générateur est correctement fermé. Pour ce faire, nous allons ajouter un bloc try-except qui intercepte l'exception GeneratorExit. L'exception GeneratorExit est levée lorsqu'un générateur est fermé, soit par la collecte de mémoire inutilisée (garbage collection) soit en appelant la méthode close().

import os
import time

def follow(filename):
    try:
        with open(filename,'r') as f:
            f.seek(0,os.SEEK_END)
            while True:
                line = f.readline()
                if line == '':
                    time.sleep(0.1)    ## Sleep briefly to avoid busy wait
                    continue
                yield line
    except GeneratorExit:
        print('Following Done')

Dans ce code modifié, le bloc try contient la logique principale du générateur. Si une exception GeneratorExit est levée, le bloc except l'intercepte et affiche le message 'Following Done'. C'est une façon simple d'effectuer des actions de nettoyage lorsque le générateur est fermé.

Enregistrez le fichier après avoir apporté ces modifications.

Expérimenter la fermeture des générateurs

Maintenant, effectuons quelques expériences pour voir comment les générateurs se comportent lorsqu'ils sont collectés par le ramasse-miettes ou fermés explicitement.

Ouvrez un terminal et lancez l'interpréteur Python :

cd ~/project
python3

Expérience 1 : Collecte de mémoire inutilisée d'un générateur en cours d'exécution

>>> from follow import follow
>>> ## Experiment: Garbage collection of a running generator
>>> f = follow('stocklog.csv')
>>> next(f)
'"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314\n'
>>> del f  ## Delete the generator object
Following Done  ## This message appears because of our GeneratorExit handler

Dans cette expérience, nous importons d'abord la fonction follow du fichier follow.py. Ensuite, nous créons un objet générateur f en appelant follow('stocklog.csv'). Nous utilisons la fonction next() pour obtenir la prochaine ligne du générateur. Enfin, nous supprimons l'objet générateur à l'aide de l'instruction del. Lorsque l'objet générateur est supprimé, il est automatiquement fermé, ce qui déclenche notre gestionnaire d'exception GeneratorExit, et le message 'Following Done' est affiché.

Expérience 2 : Fermeture explicite d'un générateur

>>> f = follow('stocklog.csv')
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         f.close()  ## Explicitly close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
Following Done
>>> for line in f:
...     print(line, end='')  ## No output: generator is closed
...

Dans cette expérience, nous créons un nouvel objet générateur f et nous itérons sur lui à l'aide d'une boucle for. À l'intérieur de la boucle, nous affichons chaque ligne et vérifions si la ligne contient la chaîne 'IBM'. Si c'est le cas, nous appelons la méthode close() sur le générateur pour le fermer explicitement. Lorsque le générateur est fermé, l'exception GeneratorExit est levée, et notre gestionnaire d'exception affiche le message 'Following Done'. Après que le générateur est fermé, si nous essayons d'itérer à nouveau sur lui, il n'y aura pas de sortie car le générateur n'est plus actif.

Expérience 3 : Sortie et reprise d'un générateur

>>> f = follow('stocklog.csv')
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         break  ## Break out of the loop, but don't close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
>>> ## Resume iteration - the generator is still active
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         break
...
"CAT",78.36,"6/11/2007","09:37.19",-0.16,78.32,78.36,77.99,237714
"VZ",42.99,"6/11/2007","09:37.20",-0.08,42.95,42.99,42.78,268459
"IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
>>> del f  ## Clean up
Following Done

Dans cette expérience, nous créons un objet générateur f et nous itérons sur lui à l'aide d'une boucle for. À l'intérieur de la boucle, nous affichons chaque ligne et vérifions si la ligne contient la chaîne 'IBM'. Si c'est le cas, nous utilisons l'instruction break pour sortir de la boucle. Sortir de la boucle ne ferme pas le générateur, donc le générateur est toujours actif. Nous pouvons ensuite reprendre l'itération en démarrant une nouvelle boucle for sur le même objet générateur. Enfin, nous supprimons l'objet générateur pour nettoyer, ce qui déclenche le gestionnaire d'exception GeneratorExit.

Points clés

  1. Lorsqu'un générateur est fermé (soit par la collecte de mémoire inutilisée soit en appelant close()), une exception GeneratorExit est levée à l'intérieur du générateur.
  2. Vous pouvez intercepter cette exception pour effectuer des actions de nettoyage lorsque le générateur est fermé.
  3. Sortir de l'itération d'un générateur (avec break) ne ferme pas le générateur, ce qui permet de le reprendre plus tard.

Quittez l'interpréteur Python en tapant exit() ou en appuyant sur Ctrl+D.

Gestion des exceptions dans les générateurs

Dans cette étape, nous allons apprendre à gérer les exceptions dans les générateurs et les coroutines. Mais d'abord, comprenons ce qu'est une exception. Une exception est un événement qui se produit lors de l'exécution d'un programme et perturbe le flux normal des instructions du programme. En Python, nous pouvons utiliser la méthode throw() pour gérer les exceptions dans les générateurs et les coroutines.

Comprendre les coroutines

Une coroutine est un type spécial de générateur. Contrairement aux générateurs ordinaires qui produisent principalement des valeurs, les coroutines peuvent à la fois consommer des valeurs (en utilisant la méthode send()) et produire des valeurs. Le fichier cofollow.py contient une implémentation simple d'une coroutine.

Ouvrons le fichier cofollow.py dans l'éditeur WebIDE. Voici le code à l'intérieur :

def consumer(func):
    def start(*args,**kwargs):
        c = func(*args,**kwargs)
        next(c)
        return c
    return start

@consumer
def printer():
    while True:
        item = yield
        print(item)

Maintenant, décomposons ce code. Le consumer est un décorateur. Un décorateur est une fonction qui prend une autre fonction en argument, lui ajoute une certaine fonctionnalité, puis retourne la fonction modifiée. Dans ce cas, le décorateur consumer déplace automatiquement le générateur jusqu'à sa première instruction yield. Cela est important car il prépare le générateur à recevoir des valeurs.

La coroutine printer() est définie avec le décorateur @consumer. À l'intérieur de la fonction printer(), nous avons une boucle while infinie. L'instruction item = yield est là que se passe la magie. Elle met en pause l'exécution de la coroutine et attend de recevoir une valeur. Lorsqu'une valeur est envoyée à la coroutine, elle reprend son exécution et affiche la valeur reçue.

Ajout de la gestion des exceptions à la coroutine

Maintenant, nous allons modifier la coroutine printer() pour gérer les exceptions. Nous allons mettre à jour la fonction printer() dans cofollow.py comme ceci :

@consumer
def printer():
    while True:
        try:
            item = yield
            print(item)
        except Exception as e:
            print('ERROR: %r' % e)

Le bloc try contient le code qui peut lever une exception. Dans notre cas, il s'agit du code qui reçoit et affiche la valeur. Si une exception se produit dans le bloc try, l'exécution saute au bloc except. Le bloc except intercepte l'exception et affiche un message d'erreur. Après avoir apporté ces modifications, enregistrez le fichier.

Expérimentation de la gestion des exceptions dans les coroutines

Commençons à expérimenter en lançant des exceptions dans la coroutine. Ouvrez un terminal et lancez l'interpréteur Python en utilisant les commandes suivantes :

cd ~/project
python3

Expérience 1 : Utilisation de base d'une coroutine

>>> from cofollow import printer
>>> p = printer()
>>> p.send('hello')  ## Send a value to the coroutine
hello
>>> p.send(42)  ## Send another value
42

Ici, nous importons d'abord la coroutine printer du module cofollow. Ensuite, nous créons une instance de la coroutine printer nommée p. Nous utilisons la méthode send() pour envoyer des valeurs à la coroutine. Comme vous pouvez le voir, la coroutine traite les valeurs que nous lui envoyons sans aucun problème.

Expérience 2 : Lancement d'une exception dans la coroutine

>>> p.throw(ValueError('It failed'))  ## Throw an exception into the coroutine
ERROR: ValueError('It failed')

Dans cette expérience, nous utilisons la méthode throw() pour injecter une exception ValueError dans la coroutine. Le bloc try-except dans la coroutine printer() intercepte l'exception et affiche un message d'erreur. Cela montre que notre gestion des exceptions fonctionne comme prévu.

Expérience 3 : Lancement d'une vraie exception dans la coroutine

>>> try:
...     int('n/a')  ## This will raise a ValueError
... except ValueError as e:
...     p.throw(e)  ## Throw the caught exception into the coroutine
...
ERROR: ValueError("invalid literal for int() with base 10: 'n/a'")

Ici, nous essayons d'abord de convertir la chaîne 'n/a' en entier, ce qui lève une exception ValueError. Nous interceptionnons cette exception et utilisons ensuite la méthode throw() pour la transmettre à la coroutine. La coroutine intercepte l'exception et affiche le message d'erreur.

Expérience 4 : Vérification que la coroutine continue de fonctionner

>>> p.send('still working')  ## The coroutine continues to run after handling exceptions
still working

Après avoir géré les exceptions, nous envoyons une autre valeur à la coroutine en utilisant la méthode send(). La coroutine est toujours active et peut traiter la nouvelle valeur. Cela montre que notre coroutine peut continuer à fonctionner même après avoir rencontré des erreurs.

Points clés

  1. Les générateurs et les coroutines peuvent gérer les exceptions au niveau de l'instruction yield. Cela signifie que nous pouvons intercepter et gérer les erreurs qui se produisent lorsque la coroutine attend ou traite une valeur.
  2. La méthode throw() vous permet d'injecter des exceptions dans un générateur ou une coroutine. Cela est utile pour les tests et pour gérer les erreurs qui se produisent en dehors de la coroutine.
  3. Gérer correctement les exceptions dans les générateurs vous permet de créer des générateurs robustes et tolérants aux erreurs qui peuvent continuer à fonctionner même en cas d'erreur. Cela rend votre code plus fiable et plus facile à maintenir.

Pour quitter l'interpréteur Python, vous pouvez taper exit() ou appuyer sur Ctrl+D.

Applications pratiques de la gestion des générateurs

Dans cette étape, nous allons explorer comment appliquer les concepts que nous avons appris sur la gestion des générateurs et la gestion des exceptions dans les générateurs à des scénarios du monde réel. Comprendre ces applications pratiques vous aidera à écrire un code Python plus robuste et plus efficace.

Création d'un système de surveillance de fichiers robuste

Construisons une version plus fiable de notre système de surveillance de fichiers. Ce système sera capable de gérer différentes situations, telles que les délais d'attente (timeouts) et les demandes de l'utilisateur d'arrêter.

Tout d'abord, ouvrez l'éditeur WebIDE et créez un nouveau fichier nommé robust_follow.py. Voici le code que vous devez écrire dans ce fichier :

import os
import time
import signal

class TimeoutError(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutError("Operation timed out")

def follow(filename, timeout=None):
    """
    A generator that yields new lines in a file.
    With timeout handling and proper cleanup.
    """
    try:
        ## Set up timeout if specified
        if timeout:
            signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(timeout)

        with open(filename, 'r') as f:
            f.seek(0, os.SEEK_END)
            while True:
                line = f.readline()
                if line == '':
                    ## No new data, wait briefly
                    time.sleep(0.1)
                    continue
                yield line
    except TimeoutError:
        print(f"Following timed out after {timeout} seconds")
    except GeneratorExit:
        print("Following stopped by request")
    finally:
        ## Clean up timeout alarm if it was set
        if timeout:
            signal.alarm(0)
        print("Follow generator cleanup complete")

Dans ce code, nous définissons d'abord une classe personnalisée TimeoutError. La fonction timeout_handler est utilisée pour lever cette erreur lorsqu'un délai d'attente expire. La fonction follow est un générateur qui lit un fichier et produit les nouvelles lignes. Si un délai d'attente est spécifié, il configure une alarme à l'aide du module signal. S'il n'y a pas de nouvelles données dans le fichier, il attend un court instant avant de réessayer. Le bloc try - except - finally est utilisé pour gérer différentes exceptions et garantir un nettoyage approprié.

Après avoir écrit le code, enregistrez le fichier.

Expérimentation avec le système de surveillance de fichiers robuste

Maintenant, testons notre système de surveillance de fichiers amélioré. Ouvrez un terminal et lancez l'interpréteur Python avec les commandes suivantes :

cd ~/project
python3

Expérience 1 : Utilisation de base

Dans l'interpréteur Python, nous allons tester la fonctionnalité de base de notre générateur follow. Voici le code à exécuter :

>>> from robust_follow import follow
>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
...     print(f"Line {i+1}: {line.strip()}")
...     if i >= 2:  ## Just read a few lines for the example
...         break
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Line 3: "HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169

Ici, nous importons la fonction follow de notre fichier robust_follow.py. Ensuite, nous créons un objet générateur f qui suit le fichier stocklog.csv. Nous utilisons une boucle for pour itérer sur les lignes produites par le générateur et afficher les trois premières lignes.

Expérience 2 : Utilisation du délai d'attente

Voyons comment fonctionne la fonctionnalité de délai d'attente. Exécutez le code suivant dans l'interpréteur Python :

>>> ## Create a generator that will time out after 3 seconds
>>> f = follow('stocklog.csv', timeout=3)
>>> for line in f:
...     print(line.strip())
...     time.sleep(1)  ## Process each line slowly
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
Following timed out after 3 seconds
Follow generator cleanup complete

Dans cette expérience, nous créons un générateur avec un délai d'attente de 3 secondes. Nous traitons chaque ligne lentement en attendant 1 seconde entre chaque ligne. Après environ 3 secondes, le générateur lève une exception de délai d'attente, et le code de nettoyage dans le bloc finally est exécuté.

Expérience 3 : Fermeture explicite

Testons comment le générateur gère une fermeture explicite. Exécutez le code suivant :

>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
...     print(f"Line {i+1}: {line.strip()}")
...     if i >= 1:
...         print("Explicitly closing the generator...")
...         f.close()
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Explicitly closing the generator...
Following stopped by request
Follow generator cleanup complete

Ici, nous créons un générateur et commençons à itérer sur ses lignes. Après avoir traité deux lignes, nous fermons explicitement le générateur en utilisant la méthode close. Le générateur gère ensuite l'exception GeneratorExit et effectue le nettoyage nécessaire.

Création d'un pipeline de traitement de données avec gestion des erreurs

Ensuite, nous allons créer un simple pipeline de traitement de données en utilisant des coroutines. Ce pipeline sera capable de gérer les erreurs à différents stades.

Ouvrez l'éditeur WebIDE et créez un nouveau fichier nommé pipeline.py. Voici le code à écrire dans ce fichier :

def consumer(func):
    def start(*args,**kwargs):
        c = func(*args,**kwargs)
        next(c)
        return c
    return start

@consumer
def grep(pattern, target):
    """Filter lines containing pattern and send to target"""
    try:
        while True:
            line = yield
            if pattern in line:
                target.send(line)
    except Exception as e:
        target.throw(e)

@consumer
def printer():
    """Print received items"""
    try:
        while True:
            item = yield
            print(f"PRINTER: {item}")
    except Exception as e:
        print(f"PRINTER ERROR: {repr(e)}")

def follow_and_process(filename, pattern):
    """Follow a file and process its contents"""
    import time
    import os

    output = printer()
    filter_pipe = grep(pattern, output)

    try:
        with open(filename, 'r') as f:
            f.seek(0, os.SEEK_END)
            while True:
                line = f.readline()
                if not line:
                    time.sleep(0.1)
                    continue
                filter_pipe.send(line)
    except KeyboardInterrupt:
        print("Processing stopped by user")
    finally:
        filter_pipe.close()
        output.close()

Dans ce code, le décorateur consumer est utilisé pour initialiser les coroutines. La coroutine grep filtre les lignes qui contiennent un motif spécifique et les envoie à une autre coroutine. La coroutine printer affiche les éléments reçus. La fonction follow_and_process lit un fichier, filtre ses lignes à l'aide de la coroutine grep et affiche les lignes correspondantes à l'aide de la coroutine printer. Elle gère également l'exception KeyboardInterrupt et garantit un nettoyage approprié.

Après avoir écrit le code, enregistrez le fichier.

Test du pipeline de traitement de données

Testons notre pipeline de traitement de données. Dans un terminal, exécutez la commande suivante :

cd ~/project
python3 -c "from pipeline import follow_and_process; follow_and_process('stocklog.csv', 'IBM')"

Vous devriez voir une sortie similaire à ceci :

PRINTER: "IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550

PRINTER: "IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859

PRINTER: "IBM",102.95,"6/11/2007","09:39.44",-0.12,102.87,102.95,102.77,225350

Cette sortie montre que le pipeline fonctionne correctement, filtrant et affichant les lignes qui contiennent le motif "IBM".

Pour arrêter le processus, appuyez sur Ctrl+C. Vous devriez voir le message suivant :

Processing stopped by user

Points clés

  1. Une gestion appropriée des exceptions dans les générateurs vous permet de créer des systèmes robustes capables de gérer les erreurs de manière gracieuse. Cela signifie que vos programmes ne planteront pas de manière inattendue en cas de problème.
  2. Vous pouvez utiliser des techniques telles que les délais d'attente pour empêcher les générateurs de s'exécuter indéfiniment. Cela aide à gérer les ressources système et garantit que votre programme ne reste pas bloqué dans une boucle infinie.
  3. Les générateurs et les coroutines peuvent former des pipelines de traitement de données puissants où les erreurs peuvent être propagées et gérées au niveau approprié. Cela facilite la construction de systèmes de traitement de données complexes.
  4. Le bloc finally dans les générateurs garantit que les opérations de nettoyage sont effectuées, quelle que soit la façon dont le générateur se termine. Cela aide à maintenir l'intégrité de votre programme et à éviter les fuites de ressources.

Résumé

Dans ce laboratoire (lab), vous avez appris des techniques essentielles pour gérer les instructions yield dans les générateurs et les coroutines Python. Vous avez exploré la gestion du cycle de vie des générateurs, y compris la gestion de l'exception GeneratorExit lors de la fermeture ou de la collecte des déchets (garbage collection) et le contrôle de l'arrêt et de la reprise de l'itération. De plus, vous avez appris à gérer les exceptions dans les générateurs, par exemple en utilisant la méthode throw() et en écrivant des générateurs robustes pour gérer les exceptions de manière gracieuse.

Ces techniques sont fondamentales pour la construction d'applications Python robustes et maintenables. Elles sont utiles pour le traitement de données, les opérations asynchrones et la gestion des ressources. En gérant correctement le cycle de vie des générateurs et en gérant les exceptions, vous pouvez créer des systèmes résilients qui gèrent les erreurs de manière gracieuse et libèrent les ressources lorsqu'elles ne sont plus nécessaires.