Personalizar la iteración utilizando generadores

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 personalizar la iteración utilizando generadores en Python. También implementarás la funcionalidad de iterador en clases personalizadas y crearás generadores para fuentes de datos en streaming.

El archivo structure.py se modificará, y se creará un nuevo archivo llamado follow.py durante el experimento.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/FileHandlingGroup -.-> python/file_reading_writing("Reading and Writing Files") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/AdvancedTopicsGroup -.-> python/iterators("Iterators") python/AdvancedTopicsGroup -.-> python/generators("Generators") subgraph Lab Skills python/conditional_statements -.-> lab-132522{{"Personalizar la iteración utilizando generadores"}} python/classes_objects -.-> lab-132522{{"Personalizar la iteración utilizando generadores"}} python/file_reading_writing -.-> lab-132522{{"Personalizar la iteración utilizando generadores"}} python/file_operations -.-> lab-132522{{"Personalizar la iteración utilizando generadores"}} python/iterators -.-> lab-132522{{"Personalizar la iteración utilizando generadores"}} python/generators -.-> lab-132522{{"Personalizar la iteración utilizando generadores"}} end

Comprendiendo los generadores de Python

Los generadores son una característica poderosa en Python. Ofrecen una forma simple y elegante de crear iteradores. En Python, cuando se trabaja con secuencias de datos, los iteradores son muy útiles ya que permiten recorrer una serie de valores uno por uno. Las funciones regulares normalmente devuelven un solo valor y luego dejan de ejecutarse. Sin embargo, los generadores son diferentes. Pueden generar una secuencia de valores con el tiempo, lo que significa que pueden producir múltiples valores de forma gradual.

¿Qué es un generador?

Una función generadora tiene una apariencia similar a una función regular. Pero la diferencia clave radica en cómo devuelve valores. En lugar de usar la declaración return para proporcionar un solo resultado, una función generadora utiliza la declaración yield. La declaración yield es especial. Cada vez que se ejecuta, el estado de la función se pausa y el valor que sigue a la palabra clave yield se devuelve al llamador. Cuando se llama a la función generadora de nuevo, se reanuda la ejecución justo donde se dejó.

Comencemos creando una función generadora simple. La función incorporada range() en Python no admite pasos fraccionarios. Entonces, crearemos una función generadora que pueda producir un rango de números con un paso fraccionario.

  1. Primero, debes abrir una nueva terminal de Python en el WebIDE. Para hacer esto, haz clic en el menú "Terminal" y luego selecciona "Nueva Terminal".
  2. Una vez que la terminal esté abierta, escribe el siguiente código en la terminal. Este código define una función generadora y luego la prueba.
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=' ')

En este código, la función frange es una función generadora. Inicializa una variable current con el valor start. Luego, siempre que current sea menor que el valor stop, produce el valor current y luego incrementa current en el valor step. El bucle for luego itera sobre los valores producidos por la función generadora frange y los imprime.

Deberías ver la siguiente salida:

0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

La naturaleza de un solo uso de los generadores

Una característica importante de los generadores es que son agotables. Esto significa que una vez que has iterado sobre todos los valores producidos por un generador, no se puede usar de nuevo para producir la misma secuencia de valores. Demostremos esto con el siguiente código:

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

En este código, primero creamos un objeto generador f utilizando la función frange. El primer bucle for itera sobre todos los valores producidos por el generador y los imprime. Después de la primera iteración, el generador se ha agotado, lo que significa que ya ha producido todos los valores que puede. Entonces, cuando intentamos iterar sobre él de nuevo en el segundo bucle for, no produce ningún valor nuevo.

Salida:

First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Second iteration:

Observa que la segunda iteración no produjo ninguna salida porque el generador ya estaba agotado.

Creando generadores reutilizables con clases

Si necesitas iterar varias veces sobre la misma secuencia de valores, puedes envolver el generador en una clase. Al hacer esto, cada vez que inicies una nueva iteración, se creará un nuevo generador.

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

En este código, definimos una clase FRange. El método __init__ inicializa los valores start, stop y step. El método __iter__ es un método especial en las clases de Python. Se utiliza para crear un iterador. Dentro del método __iter__, tenemos un generador que produce valores de manera similar a la función frange que definimos anteriormente.

Cuando creamos una instancia f de la clase FRange e iteramos sobre ella varias veces, cada iteración llama al método __iter__, que crea un nuevo generador. Entonces, podemos obtener la misma secuencia de valores varias veces.

Salida:

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

Esta vez, podemos iterar varias veces porque el método __iter__() crea un nuevo generador cada vez que se llama.

Agregando iteración a clases personalizadas

Ahora que has comprendido los conceptos básicos de los generadores, vamos a utilizarlos para agregar capacidades de iteración a clases personalizadas. En Python, si quieres hacer que una clase sea iterable, debes implementar el método especial __iter__(). Una clase iterable te permite recorrer sus elementos, al igual que puedes recorrer una lista o una tupla. Esta es una característica poderosa que hace que tus clases personalizadas sean más flexibles y fáciles de manejar.

Comprendiendo el método __iter__()

El método __iter__() es una parte crucial para hacer que una clase sea iterable. Debe devolver un objeto iterador. Un iterador es un objeto sobre el que se puede iterar (recorrer en un bucle). Una forma simple y efectiva de lograr esto es definir __iter__() como una función generadora. Una función generadora utiliza la palabra clave yield para producir una secuencia de valores uno a la vez. Cada vez que se encuentra la declaración yield, la función se pausa y devuelve el valor. La próxima vez que se llame al iterador, la función se reanuda desde donde se dejó.

Modificando la clase Structure

En la configuración de este laboratorio, proporcionamos una clase base Structure. Otras clases, como Stock, pueden heredar de esta clase Structure. La herencia es una forma de crear una nueva clase que hereda las propiedades y métodos de una clase existente. Al agregar un método __iter__() a la clase Structure, podemos hacer que todas sus subclases sean iterables. Esto significa que cualquier clase que herede de Structure tendrá automáticamente la capacidad de ser recorrida en un bucle.

  1. Abre el archivo structure.py en el WebIDE:
cd ~/project

Este comando cambia el directorio de trabajo actual al directorio project donde se encuentra el archivo structure.py. Debes estar en el directorio correcto para acceder y modificar el archivo.

  1. Observa la implementación actual de la clase 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 clase Structure tiene una lista _fields que almacena los nombres de los atributos. El método __init__() es el constructor de la clase. Inicializa los atributos del objeto comprobando si el número de argumentos pasados es igual al número de campos. Si no lo es, levanta una excepción TypeError. De lo contrario, establece los atributos utilizando la función setattr().

  1. Agrega un método __iter__() que produzca cada valor de atributo en orden:
def __iter__(self):
    for name in self._fields:
        yield getattr(self, name)

Este método __iter__() es una función generadora. Recorre la lista _fields y utiliza la función getattr() para obtener el valor de cada atributo. La palabra clave yield devuelve entonces el valor uno por uno.

El archivo structure.py completo ahora debería verse así:

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)

Esta clase Structure actualizada ahora tiene el método __iter__(), lo que la hace y a sus subclases iterables.

  1. Guarda el archivo.
    Después de realizar cambios en el archivo structure.py, debes guardarlo para que los cambios se apliquen.

  2. Ahora probemos la capacidad de iteración creando una instancia de Stock y recorriéndola:

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

Este comando crea una instancia de la clase Stock, que hereda de la clase Structure. Luego recorre la instancia utilizando una comprensión de lista e imprime cada valor.

Deberías ver una salida como esta:

Iterating over Stock:
GOOG
100
490.1

Ahora cualquier clase que herede de Structure será automáticamente iterable, y la iteración producirá los valores de los atributos en el orden definido por la lista _fields. Esto significa que puedes recorrer fácilmente los atributos de cualquier subclase de Structure sin tener que escribir código adicional para la iteración.

✨ Revisar Solución y Practicar

Mejorando las clases con capacidades de iteración

Ahora, hemos hecho que nuestra clase Structure y sus subclases admitan la iteración. La iteración es un concepto poderoso en Python que te permite recorrer una colección de elementos uno por uno. Cuando una clase admite la iteración, se vuelve más flexible y puede trabajar con muchas características incorporadas de Python. Exploremos cómo este soporte para la iteración habilita muchas características poderosas en Python.

Aprovechando la iteración para conversiones de secuencias

En Python, hay funciones incorporadas como list() y tuple(). Estas funciones son muy útiles porque pueden tomar cualquier objeto iterable como entrada. Un objeto iterable es algo sobre el que se puede iterar, como una lista, una tupla o, ahora, nuestras instancias de la clase Structure. Dado que nuestra clase Structure ahora admite la iteración, podemos convertir fácilmente sus instancias en listas o tuplas.

  1. Intentemos estas operaciones con una instancia de Stock. La clase Stock es una subclase de Structure. Ejecuta el siguiente comando en tu terminal:
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('As list:', list(s)); print('As tuple:', tuple(s))"

Este comando primero importa la clase Stock, crea una instancia de ella y luego convierte esta instancia en una lista y una tupla utilizando las funciones list() y tuple() respectivamente. La salida mostrará la instancia representada como una lista y una tupla:

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

Desempaquetado

Python tiene una característica muy útil llamada desempaquetado. El desempaquetado te permite tomar un objeto iterable y asignar sus elementos a variables individuales de una sola vez. Dado que nuestra instancia de Stock es iterable, podemos utilizar esta característica de desempaquetado en ella.

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

En este código, creamos una instancia de Stock y luego desempaquetamos sus elementos en tres variables: name, shares y price. Luego imprimimos estas variables. La salida mostrará los valores de estas variables:

Name: GOOG, Shares: 100, Price: 490.1

Agregando capacidades de comparación

Cuando una clase admite la iteración, se vuelve más fácil implementar operaciones de comparación. Las operaciones de comparación se utilizan para verificar si dos objetos son iguales o no. Vamos a agregar un método __eq__() a nuestra clase Structure para comparar instancias.

  1. Abre el archivo structure.py nuevamente. El método __eq__() es un método especial en Python que se llama cuando se utiliza el operador == para comparar dos objetos. Agrega el siguiente código a la clase Structure en el archivo structure.py:
def __eq__(self, other):
    return isinstance(other, type(self)) and tuple(self) == tuple(other)

Este método primero verifica si el objeto other es una instancia de la misma clase que self utilizando la función isinstance(). Luego convierte tanto self como other en tuplas y verifica si estas tuplas son iguales.

El archivo structure.py completo ahora debería verse así:

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. Después de agregar el método __eq__(), guarda el archivo structure.py.

  2. Probemos la capacidad de comparación. Ejecuta el siguiente comando en tu 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}')"

Este código crea tres instancias de Stock: a, b y c. Luego compara a con b y a con c utilizando el operador ==. La salida mostrará los resultados de estas comparaciones:

a == b: True
a == c: False
  1. Ahora, para asegurarnos de que todo funcione correctamente, necesitamos ejecutar las pruebas unitarias. Las pruebas unitarias son un conjunto de código que verifican si diferentes partes de tu programa funcionan como se espera. Ejecuta el siguiente comando en tu terminal:
python3 teststock.py

Si todo funciona correctamente, deberías ver una salida que indique que las pruebas han pasado:

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

OK

Al agregar solo dos métodos simples (__iter__() y __eq__()), hemos mejorado significativamente nuestra clase Structure con capacidades que la hacen más "pythonica" y más fácil de usar.

✨ Revisar Solución y Practicar

Creando un generador para datos en streaming

En programación, los generadores son una herramienta poderosa, especialmente cuando se trata de resolver problemas del mundo real, como monitorear una fuente de datos en streaming. En esta sección, aprenderemos cómo aplicar lo que hemos aprendido sobre generadores a un escenario práctico de este tipo. Vamos a crear un generador que supervise un archivo de registro y nos proporcione las nuevas líneas a medida que se agregan al archivo.

Configurando la fuente de datos

Antes de comenzar a crear el generador, necesitamos configurar una fuente de datos. En este caso, utilizaremos un programa de simulación que genera datos del mercado de valores.

Primero, debes abrir una nueva terminal en el WebIDE. Aquí es donde ejecutarás los comandos para iniciar la simulación.

Después de abrir la terminal, ejecutarás el programa de simulación de valores. Estos son los comandos que debes ingresar:

cd ~/project
python3 stocksim.py

El primer comando cd ~/project cambia el directorio actual al directorio project en tu directorio principal. El segundo comando python3 stocksim.py ejecuta el programa de simulación de valores. Este programa generará datos del mercado de valores y los escribirá en un archivo llamado stocklog.csv en el directorio actual. Deja que este programa se ejecute en segundo plano mientras trabajamos en el código de monitoreo.

Creando un simple monitor de archivos

Ahora que tenemos configurada nuestra fuente de datos, creemos un programa que supervise el archivo stocklog.csv. Este programa mostrará cualquier cambio de precio negativo.

  1. Primero, crea un nuevo archivo llamado follow.py en el WebIDE. Para hacer esto, debes cambiar el directorio al directorio project utilizando el siguiente comando en la terminal:
cd ~/project
  1. A continuación, agrega el siguiente código al archivo follow.py. Este código abre el archivo stocklog.csv, mueve el puntero del archivo al final del archivo y luego verifica continuamente si hay nuevas líneas. Si se encuentra una nueva línea y representa un cambio de precio negativo, imprime el nombre de la acción, el precio y el cambio.
## 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. Después de agregar el código, guarda el archivo. Luego, ejecuta el programa utilizando el siguiente comando en la terminal:
python3 follow.py

Deberías ver una salida que muestre las acciones con cambios de precio negativos. Podría verse algo así:

      AAPL     148.24      -1.76
      GOOG    2498.45      -1.55

Si deseas detener el programa, presiona Ctrl+C en la terminal.

Convirtiendo en una función generadora

Si bien el código anterior funciona, podemos hacerlo más reutilizable y modular convirtiéndolo en una función generadora. Una función generadora es un tipo especial de función que se puede pausar y reanudar, y que produce valores uno a la vez.

  1. Abre el archivo follow.py nuevamente y modifícalo para utilizar una función generadora. Este es el código actualizado:
## 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 función follow ahora es una función generadora. Abre el archivo, se mueve al final y luego verifica continuamente si hay nuevas líneas. Cuando se encuentra una nueva línea, la produce.

  1. Guarda el archivo y ejecútalo nuevamente utilizando el comando:
python3 follow.py

La salida debe ser la misma que antes. Pero ahora, la lógica de monitoreo de archivos está encapsulada en la función generadora follow. Esto significa que podemos reutilizar esta función en otros programas que necesiten monitorear un archivo.

Entendiendo el poder de los generadores

Al convertir nuestro código de lectura de archivos en una función generadora, lo hemos hecho mucho más flexible y reutilizable. La función follow() se puede utilizar en cualquier programa que necesite monitorear un archivo, no solo para datos de valores.

Por ejemplo, podrías utilizarla para monitorear registros de servidores, registros de aplicaciones o cualquier otro archivo que se actualice con el tiempo. Esto muestra cómo los generadores son una excelente manera de manejar fuentes de datos en streaming de forma limpia y modular.

✨ Revisar Solución y Practicar

Resumen

En este laboratorio, has aprendido cómo personalizar la iteración en Python utilizando generadores. Has creado generadores simples con la declaración yield para generar secuencias de valores, has agregado soporte para la iteración a clases personalizadas implementando el método __iter__(), has aprovechado la iteración para conversiones de secuencias, desempaquetado y comparación, y has construido un generador práctico para monitorear una fuente de datos en streaming.

Los generadores son una característica poderosa de Python que te permite crear iteradores con un código mínimo. Son especialmente útiles para procesar grandes conjuntos de datos, trabajar con datos en streaming, crear tuberías de datos (data pipelines) e implementar patrones de iteración personalizados. Utilizar generadores te permite escribir un código más limpio y eficiente en términos de memoria que expresa claramente tu intención.