Gestión de declaraciones yield en Python

PythonPythonBeginner
Practicar Ahora

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

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este laboratorio, aprenderás cómo gestionar lo que sucede en las declaraciones yield en Python. Adquirirás una comprensión de cómo manejar de manera efectiva las operaciones y comportamientos asociados a estas declaraciones.

Además, aprenderás sobre la vida útil de los generadores y el manejo de excepciones en generadores. Los archivos follow.py y cofollow.py se modificarán durante este proceso de aprendizaje.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/ErrorandExceptionHandlingGroup -.-> python/catching_exceptions("Catching Exceptions") python/ErrorandExceptionHandlingGroup -.-> python/finally_block("Finally Block") python/AdvancedTopicsGroup -.-> python/generators("Generators") subgraph Lab Skills python/catching_exceptions -.-> lab-132525{{"Gestión de declaraciones yield en Python"}} python/finally_block -.-> lab-132525{{"Gestión de declaraciones yield en Python"}} python/generators -.-> lab-132525{{"Gestión de declaraciones yield en Python"}} end

Comprender la vida útil y el cierre de los generadores

En este paso, vamos a explorar la vida útil de los generadores de Python y aprender cómo cerrarlos adecuadamente. Los generadores en Python son un tipo especial de iterador que te permite generar una secuencia de valores sobre la marcha, en lugar de calcularlos todos de una vez y almacenarlos en memoria. Esto puede ser muy útil cuando se trabaja con conjuntos de datos grandes o secuencias infinitas.

¿Qué es el generador follow()?

Comencemos por echar un vistazo al archivo follow.py en el directorio del proyecto. Este archivo contiene una función generadora llamada follow(). Una función generadora se define como una función normal, pero en lugar de usar la palabra clave return, utiliza yield. Cuando se llama a una función generadora, devuelve un objeto generador, sobre el cual se puede iterar para obtener los valores que produce.

La función generadora follow() lee continuamente líneas de un archivo y produce cada línea a medida que se lee. Esto es similar al comando Unix tail -f, que monitorea continuamente un archivo en busca de nuevas líneas.

Abre el archivo follow.py en el editor 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

En este código, la declaración with open(filename, 'r') as f abre el archivo en modo lectura y asegura que se cierre adecuadamente cuando se sale del bloque. La línea f.seek(0, os.SEEK_END) mueve el puntero del archivo al final del archivo, de modo que el generador comience a leer desde el final. El bucle while True lee continuamente líneas del archivo. Si la línea está vacía, significa que aún no hay nuevas líneas, por lo que el programa se detiene durante 0.1 segundos para evitar un ciclo activo y luego continúa con la siguiente iteración. Si la línea no está vacía, se produce.

Este generador se ejecuta en un bucle infinito, lo que plantea una pregunta importante: ¿qué sucede cuando dejamos de usar el generador o queremos terminarlo antes?

Modificar el generador para manejar el cierre

Necesitamos modificar la función follow() en follow.py para manejar el caso en el que el generador se cierre adecuadamente. Para hacer esto, agregaremos un bloque try-except que capture la excepción GeneratorExit. La excepción GeneratorExit se levanta cuando un generador se cierra, ya sea por recolección de basura o al llamar al método 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')

En este código modificado, el bloque try contiene la lógica principal del generador. Si se levanta una excepción GeneratorExit, el bloque except la captura y muestra el mensaje 'Following Done'. Esta es una forma sencilla de realizar acciones de limpieza cuando se cierra el generador.

Guarda el archivo después de realizar estos cambios.

Experimentar con el cierre de generadores

Ahora, realicemos algunos experimentos para ver cómo se comportan los generadores cuando son recolectados por el recolector de basura o se cierran explícitamente.

Abre una terminal y ejecuta el intérprete de Python:

cd ~/project
python3

Experimento 1: Recolección de basura de un generador en ejecución

>>> 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

En este experimento, primero importamos la función follow del archivo follow.py. Luego creamos un objeto generador f llamando a follow('stocklog.csv'). Usamos la función next() para obtener la siguiente línea del generador. Finalmente, eliminamos el objeto generador usando la declaración del. Cuando se elimina el objeto generador, se cierra automáticamente, lo que activa nuestro manejador de excepción GeneratorExit, y se muestra el mensaje 'Following Done'.

Experimento 2: Cerrar explícitamente un generador

>>> 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
...

En este experimento, creamos un nuevo objeto generador f e iteramos sobre él usando un bucle for. Dentro del bucle, imprimimos cada línea y verificamos si la línea contiene la cadena 'IBM'. Si es así, llamamos al método close() en el generador para cerrarlo explícitamente. Cuando se cierra el generador, se levanta la excepción GeneratorExit, y nuestro manejador de excepciones muestra el mensaje 'Following Done'. Después de que se cierra el generador, si intentamos iterar sobre él nuevamente, no habrá salida porque el generador ya no está activo.

Experimento 3: Salir y reanudar un generador

>>> 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

En este experimento, creamos un objeto generador f e iteramos sobre él usando un bucle for. Dentro del bucle, imprimimos cada línea y verificamos si la línea contiene la cadena 'IBM'. Si es así, usamos la declaración break para salir del bucle. Salir del bucle no cierra el generador, por lo que el generador sigue activo. Luego podemos reanudar la iteración iniciando un nuevo bucle for sobre el mismo objeto generador. Finalmente, eliminamos el objeto generador para realizar la limpieza, lo que activa el manejador de excepción GeneratorExit.

Puntos clave

  1. Cuando se cierra un generador (ya sea a través de la recolección de basura o llamando a close()), se levanta una excepción GeneratorExit dentro del generador.
  2. Puedes capturar esta excepción para realizar acciones de limpieza cuando se cierra el generador.
  3. Salir de la iteración de un generador (con break) no cierra el generador, lo que permite reanudarlo más tarde.

Sal del intérprete de Python escribiendo exit() o presionando Ctrl+D.

✨ Revisar Solución y Practicar

Manejo de excepciones en generadores

En este paso, aprenderemos cómo manejar excepciones en generadores y corutinas. Pero primero, entendamos qué son las excepciones. Una excepción es un evento que ocurre durante la ejecución de un programa y altera el flujo normal de las instrucciones del programa. En Python, podemos usar el método throw() para manejar excepciones en generadores y corutinas.

Comprender las corutinas

Una corutina es un tipo especial de generador. A diferencia de los generadores regulares que principalmente producen valores, las corutinas pueden tanto consumir valores (usando el método send()) como producir valores. El archivo cofollow.py tiene una implementación sencilla de una corutina.

Abriremos el archivo cofollow.py en el editor WebIDE. Aquí está el código dentro:

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)

Ahora, desglosemos este código. El consumer es un decorador. Un decorador es una función que toma otra función como argumento, le agrega alguna funcionalidad y luego devuelve la función modificada. En este caso, el decorador consumer mueve automáticamente el generador a su primera declaración yield. Esto es importante porque hace que el generador esté listo para recibir valores.

La corutina printer() se define con el decorador @consumer. Dentro de la función printer(), tenemos un bucle while infinito. La declaración item = yield es donde ocurre la magia. Pausa la ejecución de la corutina y espera a recibir un valor. Cuando se envía un valor a la corutina, se reanuda la ejecución y se imprime el valor recibido.

Agregar manejo de excepciones a la corutina

Ahora, modificaremos la corutina printer() para manejar excepciones. Actualizaremos la función printer() en cofollow.py de la siguiente manera:

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

El bloque try contiene el código que podría generar una excepción. En nuestro caso, es el código que recibe e imprime el valor. Si ocurre una excepción en el bloque try, la ejecución salta al bloque except. El bloque except captura la excepción e imprime un mensaje de error. Después de realizar estos cambios, guarda el archivo.

Experimentar con el manejo de excepciones en corutinas

Comencemos a experimentar lanzando excepciones a la corutina. Abre una terminal y ejecuta el intérprete de Python usando los siguientes comandos:

cd ~/project
python3

Experimento 1: Uso básico de la corutina

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

Aquí, primero importamos la corutina printer del módulo cofollow. Luego creamos una instancia de la corutina printer llamada p. Usamos el método send() para enviar valores a la corutina. Como se puede ver, la corutina procesa los valores que le enviamos sin problemas.

Experimento 2: Lanzar una excepción a la corutina

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

En este experimento, usamos el método throw() para inyectar una excepción ValueError en la corutina. El bloque try-except en la corutina printer() captura la excepción e imprime un mensaje de error. Esto muestra que nuestro manejo de excepciones funciona como se espera.

Experimento 3: Lanzar una excepción real a la corutina

>>> 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'")

Aquí, primero intentamos convertir la cadena 'n/a' a un entero, lo que genera una excepción ValueError. Capturamos esta excepción y luego usamos el método throw() para pasarla a la corutina. La corutina captura la excepción e imprime el mensaje de error.

Experimento 4: Verificar que la corutina sigue funcionando

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

Después de manejar las excepciones, enviamos otro valor a la corutina usando el método send(). La corutina sigue activa y puede procesar el nuevo valor. Esto muestra que nuestra corutina puede seguir funcionando incluso después de encontrar errores.

Puntos clave

  1. Los generadores y las corutinas pueden manejar excepciones en el punto de la declaración yield. Esto significa que podemos capturar y manejar errores que ocurren cuando la corutina está esperando o procesando un valor.
  2. El método throw() te permite inyectar excepciones en un generador o corutina. Esto es útil para pruebas y para manejar errores que ocurren fuera de la corutina.
  3. Manejar adecuadamente las excepciones en generadores te permite crear generadores robustos y tolerantes a errores que pueden seguir funcionando incluso cuando se producen errores. Esto hace que tu código sea más confiable y fácil de mantener.

Para salir del intérprete de Python, puedes escribir exit() o presionar Ctrl+D.

✨ Revisar Solución y Practicar

Aplicaciones prácticas de la gestión de generadores

En este paso, exploraremos cómo aplicar los conceptos que hemos aprendido sobre la gestión de generadores y el manejo de excepciones en generadores a escenarios del mundo real. Comprender estas aplicaciones prácticas te ayudará a escribir código Python más robusto y eficiente.

Crear un sistema de monitoreo de archivos robusto

Construyamos una versión más confiable de nuestro sistema de monitoreo de archivos. Este sistema será capaz de manejar diferentes situaciones, como tiempos de espera (timeouts) y solicitudes del usuario para detenerlo.

Primero, abre el editor WebIDE y crea un nuevo archivo llamado robust_follow.py. Aquí está el código que debes escribir en este archivo:

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")

En este código, primero definimos una clase personalizada TimeoutError. La función timeout_handler se utiliza para lanzar este error cuando se produce un tiempo de espera. La función follow es un generador que lee un archivo y produce nuevas líneas. Si se especifica un tiempo de espera, configura una alarma utilizando el módulo signal. Si no hay nuevos datos en el archivo, espera un corto tiempo antes de intentarlo de nuevo. El bloque try - except - finally se utiliza para manejar diferentes excepciones y garantizar una limpieza adecuada.

Después de escribir el código, guarda el archivo.

Experimentar con el sistema de monitoreo de archivos robusto

Ahora, probemos nuestro sistema de monitoreo de archivos mejorado. Abre una terminal y ejecuta el intérprete de Python con los siguientes comandos:

cd ~/project
python3

Experimento 1: Uso básico

En el intérprete de Python, probaremos la funcionalidad básica de nuestro generador follow. Aquí está el código a ejecutar:

>>> 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

Aquí, importamos la función follow de nuestro archivo robust_follow.py. Luego creamos un objeto generador f que sigue el archivo stocklog.csv. Usamos un bucle for para iterar sobre las líneas producidas por el generador e imprimimos las primeras tres líneas.

Experimento 2: Uso del tiempo de espera

Veamos cómo funciona la función de tiempo de espera. Ejecuta el siguiente código en el intérprete de 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

En este experimento, creamos un generador con un tiempo de espera de 3 segundos. Procesamos cada línea lentamente, esperando 1 segundo entre cada línea. Después de aproximadamente 3 segundos, el generador lanza una excepción de tiempo de espera y se ejecuta el código de limpieza en el bloque finally.

Experimento 3: Cierre explícito

Probemos cómo el generador maneja un cierre explícito. Ejecuta el siguiente código:

>>> 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

Aquí, creamos un generador y comenzamos a iterar sobre sus líneas. Después de procesar dos líneas, cerramos explícitamente el generador utilizando el método close. El generador luego maneja la excepción GeneratorExit y realiza la limpieza necesaria.

Crear una tubería de procesamiento de datos con manejo de errores

A continuación, crearemos una simple tubería de procesamiento de datos utilizando corutinas. Esta tubería será capaz de manejar errores en diferentes etapas.

Abre el editor WebIDE y crea un nuevo archivo llamado pipeline.py. Aquí está el código a escribir en este archivo:

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()

En este código, el decorador consumer se utiliza para inicializar corutinas. La corutina grep filtra las líneas que contienen un patrón específico y las envía a otra corutina. La corutina printer imprime los elementos recibidos. La función follow_and_process lee un archivo, filtra sus líneas utilizando la corutina grep e imprime las líneas coincidentes utilizando la corutina printer. También maneja la excepción KeyboardInterrupt y garantiza una limpieza adecuada.

Después de escribir el código, guarda el archivo.

Probar la tubería de procesamiento de datos

Probemos nuestra tubería de procesamiento de datos. En una terminal, ejecuta el siguiente comando:

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

Deberías ver una salida similar a esta:

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

Esta salida muestra que la tubería está funcionando correctamente, filtrando e imprimiendo las líneas que contienen el patrón "IBM".

Para detener el proceso, presiona Ctrl+C. Deberías ver el siguiente mensaje:

Processing stopped by user

Puntos clave

  1. El manejo adecuado de excepciones en generadores te permite crear sistemas robustos que pueden manejar errores de manera elegante. Esto significa que tus programas no se bloquearán inesperadamente cuando algo salga mal.
  2. Puedes utilizar técnicas como los tiempos de espera para evitar que los generadores se ejecuten indefinidamente. Esto ayuda a administrar los recursos del sistema y garantiza que tu programa no se quede atrapado en un bucle infinito.
  3. Los generadores y las corutinas pueden formar poderosas tuberías de procesamiento de datos donde los errores pueden propagarse y manejarse en el nivel adecuado. Esto facilita la construcción de sistemas de procesamiento de datos complejos.
  4. El bloque finally en los generadores garantiza que se realicen operaciones de limpieza, independientemente de cómo se termine el generador. Esto ayuda a mantener la integridad de tu programa y evita fugas de recursos.
✨ Revisar Solución y Practicar

Resumen

En este laboratorio (lab), has aprendido técnicas esenciales para gestionar las declaraciones yield en generadores y corutinas de Python. Has explorado la gestión del ciclo de vida de los generadores, incluyendo el manejo de la excepción GeneratorExit durante el cierre o la recolección de basura, y el control del inicio y la interrupción de la iteración. Además, has aprendido sobre el manejo de excepciones en generadores, como el uso del método throw() y la escritura de generadores robustos para manejar excepciones de manera elegante.

Estas técnicas son fundamentales para construir aplicaciones de Python robustas y mantenibles. Son útiles para el procesamiento de datos, operaciones asíncronas y la gestión de recursos. Al gestionar adecuadamente el ciclo de vida de los generadores y manejar las excepciones, puedes crear sistemas resistentes que manejen los errores de manera elegante y liberen recursos cuando ya no son necesarios.