Generadores en Python
CategoriasProgramaciónPython

Generadores en Python

Generadores Python

Un generador es una función “especial” que produce valores uno a uno.

Cada vez que llamemos a esta función, nos devolverá un nuevo valor.

Para una función normal se utiliza return para devolver un valor, pues bien, para crear un generador se utiliza yield para devolver un valor.

Una diferencia importante es que yield devuelve un valor y suspende la ejecución de la función, es decir, pausa la función y mantiene el estado de sus variables hasta que volvamos a llamar al generador.

Lo vemos mejor con un ejemplo:

def mi_generador():
    n = 1
    print('Primera ejecución')
    yield n

    n += 1
    print('Segunda ejecución')
    yield n

    n += 1
    print('Tercera ejecución')
    yield n

gen = mi_generador()
print(next(gen))  # Primera ejecución, devuelve 1
print(next(gen))  # Segunda ejecución, devuelve 2
print(next(gen))  # Tercera ejecución, devuelve 3

Expliquemos un poco.

Lo que hacemos es definir un generador llamado “mi_generador”. Dentro de esta función (generador), usamos la palabra clave yield tres veces: cada vez que incrementamos el valor de “n” e imprimimos un mensaje.

Con gen = mi_generador() creamos una instancia del generador.

Y con print(next(gen)) se produce la primera llamada al generador y en nuestro ejemplo devuelve 1.

Si nos fijamos hacemos uso de la función next(), que nos sirve para avanzar al siguiente valor producido por el generador.

La primera llamada a print(next(gen)), hace que la función mi_generador se ejecute hasta llegar a la primera expresión yield. Entonces:

  • Se establece “n” en 1.
  • Imprime “Primera ejecución”.
  • La función queda suspendida en yield n y devuelve el valor de “n”, que es 1.

Por eso le resultado final es:

Primera ejecución

1

Si a continuación volvemos a ejecutar print(next(gen)), esta vez por segunda vez, el generador se reanuda justo después de la primera expresión yield, es decir:

  • “n” se incrementa en 1, por tanto, “n” ahora es 2.
  • Se imprime “Segunda ejecución”.
  • La función (o el generador) se vuelve a suspender en yield n y devuelve el valor de “n”, que ahora es 2.

Ahora, el resultado final es:

Segunda ejecución

2

¿La tercera llamada a print(next(gen)) creo que ya es bastante obvia no?

El generador se vuelve a reanudar después de la segunda expresión yield, haciendo que:

  • “n” se incremente en 1 y por tanto, ahora “n” valga 3.
  • Se imprime “Tercera ejecución”.
  • El generador se vuelve a suspender en yield n devolviendo el valor de “n”, que es 3.

El resultado que imprime por pantalla es (obvio):

Tercera ejecución

3

Todo este ejemplo ha servido para ver lo que había dicho al principio del post: “yield devuelve un valor y suspende la ejecución de la función, manteniendo el estado de sus variables hasta volver a llamar al generador.

Por cierto, si ejecutáramos print(next(gen)) por una cuarta vez, al no haber más expresiones yield, se lanzaría la excepción “StopIteration” indicándonos que el generador ha terminado de producir valores.

Como hemos visto, next() nos permite controlar la ejecución del generador y obtener los valores que se producen, uno cada vez. Cada vez que llamamos a next() se avanza la ejecución de la función generadora hasta el próximo yield, devolviendo el valor producido y suspendiendo la ejecución nuevamente.

También tenemos otros métodos adicionales como son send() y close():

send(value): Nos permite enviar un valor al generador. Convirtiendo el valor enviado en el resultado de la expresión yield.

def generador_con_send():
    valor = yield "Iniciar"
    while True:
        valor = yield f"Valor recibido: {valor}"

gen = generador_con_send()
print(next(gen))  # "Iniciar"
print(gen.send(10))  # "Valor recibido: 10"
print(gen.send(20))  # "Valor recibido: 20"

close(): Termina el generador.

A parte de los ejemplos anteriores, un generador nos puede servir para varias cosas más como por ejemplo usarlos en algún bucle para generar una secuencia de valores tal que así:

def contador(n):
    i = 0
    while i < n:
        yield i
        i += 1

for numero in contador(10):
    print(numero)  # Imprime números del 0 al 9

Si te preguntas: ¡eso también puedo hacerlo con una lista!

Te digo: ¡Tienes toda la razón! Puedes crear una lista y luego iterar sobre esa lista.

Ejemplo:

def contador(n):
    numeros = []
    i = 0
    while i < n:
        numeros.append(i)
        i += 1
    return numeros

for numero in contador(10):
    print(numero)  # Imprime números del 0 al 9

Ahora bien, las dos funciones hacen lo mismo, pero he aquí la principal diferencia de hacerlo de una manera u otra: la memoria.

  • El generador hace un uso más eficiente de la memoria porque genera los elementos sobre la marcha manteniendo solo un elemento en memoria a la vez.
  • La lista guarda todos los elementos en memoria, así que si “n” es muy grande puede suponer un consumo bastante significativo.

O sea que como vemos, un generador tiene la ventaja de ahorrarnos memoria ya que produce los elementos uno a uno y solo cuando se necesitan, lo que es mucho más eficiente que crear y almacenar toda la lista de elementos a la vez.

Pero no solo eso, el código es más simple. Escribimos menos y es más fácil de leer.

Ahora te puedes preguntar, ¿Cuándo uso uno u otro? ¡Fácil!

Si estás trabajando con grandes conjuntos de datos y quieres minimizar el uso de memoria usa generadores.

Si, por el contrario, la memoria no te preocupa, el conjunto de datos es pequeño o necesitas acceder a todos los elementos generados al mismo tiempo, entonces usa listas.

Otros usos pueden ser por ejemplo para crear una función (generador) que produzca números pares:

def generador_pares(limite):
    n = 0
    while n < limite:
        yield n
        n += 2

pares = generador_pares(10)
for numero in pares:
    print(numero)

O bien que itere sobre los caracteres de una cadena:

def generador_caracteres(cadena):
    for caracter in cadena:
        yield caracter

caracteres = generador_caracteres("oscardev.net")
for caracter in caracteres:
    print(caracter)

El resultado sería:

o

s

c

a

r

d

e

v

.

n

e

t

También nos puede ser útil para leer un archivo línea por línea:

def generador_lectura_archivo(ruta_archivo):
    with open(ruta_archivo, 'r') as archivo:
        for linea in archivo:
            yield linea.strip()

for linea in generador_lectura_archivo(ejemplo.txt'):
    print(linea)

Y por ahora, hasta aquí el tema de los generadores. Como puedes ver, todos estos ejemplos nos muestran cómo los generadores pueden usarse para una variedad de propósitos: generar secuencias numéricas, iterar sobre archivos, etc. Y todo eso, de manera eficiente.

Sobre el autor

Comparte:

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Información básica sobre protección de datos
Responsable Óscar Martínez +info...
Finalidad Gestionar y moderar los comentarios +info...
Legitimación Consentimiento del interesado. +info...
Destinatarios No se cederán datos a terceros, salvo obligación legal +info...
Derechos Acceder, rectificar y cancelar los datos, así como otros derechos. +info...
Información adicional Puedes consultar la información adicional y detallada sobre protección de datos en nuestra página de política de privacidad.

Este sitio esta protegido por reCAPTCHA y laPolítica de privacidady losTérminos del servicio de Googlese aplican.

El periodo de verificación de reCAPTCHA ha caducado. Por favor, recarga la página.