Saltar al contenido principal

Palabra clave de rendimiento Python: ¿Qué es y cómo se utiliza?

La palabra clave yield en Python convierte una función regular en un generador, que produce una secuencia de valores bajo demanda en lugar de calcularlos todos a la vez.
Actualizado 29 jul 2024

Las funciones de Python no siempre tienen una declaración return. Las funciones generadoras son funciones que tienen la palabra clave yield en lugar de return.

Estas funciones producen iteradores generadores, que son objetos que representan un flujo de datos. Los elementos representados por un iterador se crean y ceden sólo cuando es necesario. Este tipo de evaluación suele denominarse evaluación perezosa.

Cuando se trata de grandes conjuntos de datos, los generadores ofrecen una alternativa eficiente en memoria al almacenamiento de datos en listas, tuplas y otras estructuras de datos que requieren espacio en memoria para cada uno de sus elementos. Las funciones generadoras también pueden crear iteradores infinitos, lo que no es posible con estructuras de evaluación rápida como listas y tuplas.

Antes de empezar, recapitulemos las diferencias entre funciones y generadores:

Función

Función

Generador

Valor Producción

Devuelve todos los valores a la vez

Produce valores de uno en uno, bajo demanda

Ejecución

Se ejecuta completamente antes de volver

Se detiene después de ceder, se reanuda cuando se solicita el siguiente valor

Palabra clave

devuelve

yield

Uso de la memoria

Potencialmente alto, almacena toda la secuencia en la memoria

Bajo, almacena sólo el valor actual y el estado para el siguiente

Iteración

Es posible realizar múltiples iteraciones, pero requiere almacenar toda la secuencia

Diseñado para la iteración de una sola pasada, más eficaz para secuencias grandes o infinitas

Utilizando la función de Python yield para Crear Funciones Generadoras

El término generador en Python puede referirse a un iterador generador o a una función generadora. Son objetos diferentes pero relacionados en Python. En este tutorial se utilizan a menudo los términos completos para evitar confusiones.

Exploremos primero las funciones generadoras. Una función generadora tiene un aspecto similar al de una función normal, pero contiene la palabra clave yield en lugar de return.

Cuando un programa Python llama a una función generadora, crea un iterador generador. Los iteradores proporcionan un valor bajo demanda y detienen su ejecución hasta que se necesite otro valor. Veamos un ejemplo para explicar este concepto y demostrar la diferencia entre funciones regulares y funciones generadoras.

Utilizar una función regular

En primer lugar, vamos a definir una función regular, que contiene una declaración return. Esta función acepta una secuencia de palabras y una letra, y devuelve una lista con el número de apariciones de la letra en cada palabra:

def find_letter_occurrences(words, letter):
    output = []
    for word in words:
        output.append(word.count(letter))
    return output
print(
    find_letter_occurrences(["apple", "banana", "cherry"], "a")
)
[1, 3, 0]

La función da como resultado una lista que contiene 1, 3 y 0, ya que hay una a en manzana, tres apariciones de a en plátano y ninguna en cereza. La misma función puede refactorizarse para utilizar comprensiones de lista en lugar de inicializar una lista vacía y utilizar .append():

def find_letter_occurrences(words, letter):
    return [word.count(letter) for word in words]

Esta función regular devuelve una lista con todos los resultados cada vez que se llama. Sin embargo, si la lista de palabras es grande, llamar a esta función regular exige más memoria, ya que el programa crea y almacena una nueva lista del mismo tamaño que la original. Si esta función se utiliza repetidamente con varios argumentos de entrada, o funciones similares realizan otras operaciones con los datos originales, la presión sobre la memoria puede aumentar rápidamente.

Utilizar una función generadora

En su lugar se puede utilizar una función generadora:

def find_letter_occurrences(words, letter):
    for word in words:
        yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print(output)
<generator object find_letter_occurrences at 0x102935e00>

La función incluye la palabra claveyield en lugar de return. Esta función generadora devuelve un objeto generador cuando se llama, que se asigna a output. Este objeto es un iterador. No contiene los datos que representan el número de apariciones de la letra en cada palabra. En su lugar, el generador creará y cederá los valores cuando sea necesario. Vamos a obtener el primer valor de este iterador generador:

print(next(output))
1

La función incorporada next() es una forma de obtener el siguiente valor de un iterador. Veremos otras formas más adelante en este tutorial.

El código de la función generadora se ejecuta hasta que el programa llega a la línea con la palabra clave yield. En este ejemplo, el bucle for inicia su primera iteración y obtiene el primer elemento de la lista words. El método de cadena .count() devuelve un número entero, que en este caso es 1, ya que hay una aparición de a en manzana. El generador produce este valor, que es devuelto por next(output).

El generador output detiene su ejecución en este punto. Por lo tanto, el generador completó la primera iteración del bucle for y encontró el número de apariciones de la letra a en la primera palabra de la lista de palabras. Ahora está esperando hasta que se la necesite de nuevo.

Si se vuelve a llamar al built-in next() con output como argumento, el generador reanudará la ejecución desde el punto en que se detuvo:

print(next(output))
3

El generador continúa desde la línea con yield en la primera iteración del bucle for. Como el bucle for no tiene más líneas de código, vuelve al principio del bucle y busca el segundo elemento de la lista words. El valor devuelto por .count() es 3 en este caso, y se obtiene este valor. El generador se detiene de nuevo en este punto de la ejecución.

La tercera llamada a next() reanuda esta ejecución:

print(next(output))
0

La segunda iteración llega al final del bucle for, que pasa a la tercera iteración. El código avanza de nuevo hasta la línea con yield, que esta vez devuelve el entero 0, ya que no hay ninguna ocurrencia de a en cherry.

El generador se detiene de nuevo. El programa sólo determina el destino del generador cuando llamamos por cuarta vez a next():

print(next(output))
Traceback (most recent call last):
  ...
StopIteration

La ejecución se reanuda desde el final de la tercera iteración del bucle for. Sin embargo, el bucle ha llegado al final de su iteración, ya que no hay más elementos en la lista words. El generador lanza una excepción StopIteration.

En la mayoría de los casos de uso, no se accede directamente a los elementos generadores mediante next(), sino a través de otro proceso de iteración. La excepción StopIteration señala el final del proceso de iteración. Exploraremos esto más a fondo en la siguiente sección de este tutorial.

Python tiene otra forma de crear iteradores generadores cuando su funcionamiento puede representarse con una sola expresión, como en el ejemplo anterior. El iterador generador output puede crearse utilizando una _expresión generadora_:

words = ["apple", "banana", "cherry"]
letter = "a"
output = (word.count(letter) for word in words)
print(next(output))
print(next(output))
print(next(output))
print(next(output))
1
3
0
Traceback (most recent call last):
  ...
StopIteration

La expresión entre paréntesis asignada a output es una expresión generadora, que crea un iterador generador similar al producido por la función generadora find_letter_occurrences().

Concluyamos esta sección con otro ejemplo de función generadora para destacar cómo la ejecución se detiene y se reanuda cada vez que se necesita un elemento:

def show_status():
    print("Start")
    yield
    print("Middle")
    yield
    print("End")
    yield
status = show_status()
next(status)
Start

Esta función generadora no tiene bucle. En su lugar, contiene tres líneas que tienen la palabra clave yield. El código crea un iterador generador status cuando llama a la función generadora show_status(). La primera vez que el programa llama a next(status), el generador inicia la ejecución. Imprime la cadena "Start" y realiza una pausa tras la primera expresión yield. El generador da None, ya que no hay ningún objeto a continuación de la palabra clave yield.

El programa imprime la cadena "Middle" sólo cuando se llama por segunda vez a next():

next(status)
Middle

El generador se detiene después de la segunda expresión yield. La tercera llamada a next() imprime la cadena final, "End":

next(status)
End

El generador se detiene en la expresión final yield. Lanzará una excepción StopIteration la próxima vez que el programa solicite un valor a este iterador generador:

next(status)
Traceback (most recent call last):
  ...
StopIteration

Exploraremos más formas de utilizar los generadores en la siguiente sección.

Trabajar con iteradores generadores

Las funciones generadoras crean iteradores generadores, y los iteradores son iterables. Desgranemos esta frase. Cada vez que el programa llama a una función generadora, crea un iterador. Como los iteradores son iterables, pueden utilizarse en los bucles for y otros procesos iterativos.

Por tanto, la función incorporada next() no es la única forma de acceder a los elementos de un iterador. Esta sección explora otras formas de trabajar con generadores.

Utilizar el protocolo de iteración de Python con iteradores generadores

Volvamos a una función generadora de una sección anterior de este tutorial:

def find_letter_occurrences(words, letter):
    for word in words:
        yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
for value in output:
    print(value)
1
3
0

En lugar de utilizar next() varias veces, esta versión del código utiliza el iterador generador output en un bucle for. Como los iteradores son iterables, pueden utilizarse en los bucles for. El bucle recupera elementos del iterador generador hasta que no queda ningún valor.

A diferencia de las estructuras de datos como las listas y las tuplas, un iterador sólo puede utilizarse una vez. El código no vuelve a imprimir los valores si intentamos ejecutar el mismo bucle for por segunda vez:

def find_letter_occurrences(words, letter):
    for word in words:
        yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print("First attempt:")
for value in output:
    print(value)
print("Second attempt:")
for value in output:
    print(value)
First attempt:
1
3
0
Second attempt:

El iterador se agota en el primer bucle for, por lo que ya no puede dar valores. Si se vuelve a necesitar el generador una vez agotado, debemos crear otro iterador generador a partir de la función generador.

También es posible que existan varios iteradores generadores al mismo tiempo en un programa:

 
def find_letter_occurrences(words, letter):
    for word in words:
        yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
first_output = find_letter_occurrences(words, letter)
second_output = find_letter_occurrences(words, letter)
print("First value of first_output:")
print(next(first_output))
print("Values of second_output:")
for value in second_output:
    print(value)
print("Remaining values of first_output:")
for value in first_output:
    print(value)
First value of first_output:
1
Values of second_output:
1
3
0
Remaining values of first_output:
3
0

La función generadora find_letter_occurrences() crea dos iteradores generadores: first_output y second_output. Aunque ambos iteradores se refieren a los mismos datos de la lista words, progresan independientemente el uno del otro.

Este ejemplo obtiene el primer valor de first_output utilizando next(). El iterador generador da 1 y se detiene en este punto. A continuación, el programa pasa por second_output. Como este generador aún no ha producido ningún valor, el bucle recorre todos los valores producidos por el segundo iterador. Por último, hay otro bucle for que itera a través de first_output. Sin embargo, este iterador ya ha dado su primer valor anteriormente en el programa. El bucle recorre los valores restantes de este iterador.

El bucle for no es el único proceso que puede utilizarse para iterar a través de iteradores generadores:

print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
1 3 0
[0, 1, 3]

En estos ejemplos, el programa llama directamente a la función generador para crear y utilizar el iterador generador, en lugar de asignarlo a una variable. En el primer ejemplo, el iterador se descomprime utilizando la notación estrella. Este proceso se basa en el mismo protocolo de iteración que el bucle for.

En el segundo ejemplo, el iterador generador se pasa a la función sorted(), que requiere un argumento iterable. Los generadores son iterables y, por tanto, pueden utilizarse siempre que se produzca la iteración de Python.

Crear iteradores infinitos

Un generador produce un valor y hace una pausa hasta que se necesita el siguiente valor. Cada vez que el código solicite un valor a un iterador, el código de la función generadora se ejecutará hasta que se evalúe la siguiente expresión yield. En todos los ejemplos de este tutorial hasta ahora, la función generadora tenía un número finito de expresiones yield. Sin embargo, es posible crear un generador que produzca un número infinito de valores utilizando un bucle while en la función generadora. En el ejemplo siguiente, el generador obtiene un color aleatorio de la lista de colores que se pasa a la función generadora:

import random
def get_color(colors):
    while True:
        yield random.choice(colors)
output_colors = get_color(["red", "green", "blue"])
print("First two colors:")
print(next(output_colors))
print(next(output_colors))
print("Next 10 colors using a 'for' loop:")
for _ in range(10):
    print(next(output_colors))
First two colors:
green
red
Next 10 colors using a 'for' loop:
blue
green
green
green
red
red
red
blue
green
red

La función generadora get_color() tiene una expresión yield dentro de un bucle while. Por lo tanto, el código siempre se encontrará con otra expresión yield cuando busque el siguiente valor. El iterador generador output_colors produce un número infinito de colores elegidos al azar de la lista de entrada. Este generador nunca se agotará.

No es posible crear estructuras de datos infinitas, como listas y tuplas. Los generadores permiten a un programa crear infinitos iterables. Ten en cuenta que si el iterador generador se utiliza directamente dentro de un bucle for, el bucle se ejecutará eternamente.

Conceptos avanzados de generadores

Los generadores tienen casos de uso más avanzados en Python. Esta sección explorará algunas de ellas.

Enviar un objeto al generador

Los generadores también pueden aceptar datos adicionales que pueden utilizarse al evaluar el código. La sentencia que contiene la palabra clave yield es una expresión que se evalúa a un valor. Este valor se puede asignar a una variable dentro de la función generadora. Empecemos con un ejemplo básico para demostrar este concepto:

def generator_function():
    value = yield 1
    print(f"The yield expression evaluates to: {value}")
    value = yield 2
    print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(next(output))
print(next(output))
1
The yield expression evaluates to: None
2
The yield expression evaluates to: None
Traceback (most recent call last):
  ...
StopIteration

La palabra clave de Python yield crea una expresión que se evalúa a un valor. Sin embargo, el valor de esta expresión dentro de la función generadora no es el mismo objeto que produce el generador. Considera la primera expresión yield. El generador produce el entero 1. Por lo tanto, print(next(output)) muestra 1 la primera vez que se llama y detiene la ejecución del generador.

Sin embargo, la expresión yield del generador evalúa a un objeto, que el código asigna al nombre de variable value. En este ejemplo, yield asigna None a value. Este proceso se repite para la segunda aparición de yield en la función generadora. El objetivo de la tercera llamada a next() es garantizar que se ejecuta todo el código de la función generadora.

Sustituyamos la segunda y tercera llamadas a next() por .send()que es un método de la clase generador:

def generator_function():
    value = yield 1
    print(f"The yield expression evaluates to: {value}")
    value = yield 2
    print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(output.send("Here's a value"))
print(output.send("Here's another value"))
1
The yield expression evaluates to: Here's a value
2
The yield expression evaluates to: Here's another value
Traceback (most recent call last):
  ...
StopIteration

La función generadora no cambia. El generador se inicia llamando a next(), y el código se ejecuta hasta que da como resultado el primer entero, 1. En lugar de utilizar next() la segunda vez, el programa llama a output.send(). Este método envía un objeto al generador. En este ejemplo, el objeto es una cadena. La expresión yield de la función generadora evalúa esta cadena, que se asigna a value. Por tanto, el generador puede utilizar la cadena dentro de su código.

La segunda llamada a .send() envía un nuevo objeto al generador, que se asigna a la misma variable value. El generador emite un StopIteration tras la última llamada a print(), ya que no hay más expresiones yield.

Veamos otro ejemplo utilizando .send(). El siguiente generador muestra el saldo de una cuenta, pero el saldo se puede actualizar:

def get_balance(start_balance):
    balance = start_balance
    while True:
        amount = yield balance
        if amount is not None:
            balance += amount
current_balance = get_balance(100)
print(next(current_balance))
print(current_balance.send(10))
print(current_balance.send(-20))
print(next(current_balance))
100
110
90
90

La función generador requiere un saldo inicial cuando se llama. El valor de balance puede cambiar a medida que el generador ejecuta el código. Cualquier objeto enviado al generador mediante .send() se asigna a amount. Esta variable será None si el generador devuelve un valor sin que se le haya enviado ningún objeto, o contendrá el objeto enviado mediante .send().

El iterador generador current_balance comienza con un saldo de 100$. El generador se inicia llamando a next(), que comienza a ejecutar el código hasta que se obtiene el primer valor.

Una vez iniciado el generador, es posible reiniciar la ejecución utilizando .send() en lugar de next(). El generador añade el valor enviado a la balanza. Si no se envía ningún valor, por ejemplo llamando de nuevo a next(), el generador devuelve el saldo sin modificar.

Rendimiento directo desde otro iterable

Los generadores de Python también pueden obtener valores directamente de otro generador o iterable utilizando la sintaxis yield from. Veamos un ejemplo de función generadora que devuelve valores de una lista anidada:

def flatten(nested_list):
    for item in nested_list:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item
nested_list = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested_list)))
[1, 2, 3, 4, 5, 6, 7]

La función generador acepta una lista, que puede incluir listas anidadas en su interior. El bucle for recorre los elementos de la lista. Cada elemento de la lista exterior es un valor, en este caso un entero, u otra lista. Cuando el elemento no es una lista, el generador devuelve el elemento.

Sin embargo, cuando el elemento es una lista, el generador vuelve a llamar recursivamente a la función generadora flatten() con la lista interior como argumento. Esto crea otro iterador generador, que utiliza la lista interior como fuente de datos. Si esta línea utilizara una expresión yield, el primer generador daría lugar al segundo generador. En cambio, utilizando yield from, el primer generador obtiene valores del segundo generador.

Resumen: yield vs. return

Las definiciones de funciones con return y yield tienen un aspecto similar, pero su comportamiento es diferente. Resumamos las principales diferencias:

 

Función regular

Función Generador

Palabra clave

retorno (implícito si no se utiliza explícitamente)

yield

Llamado

Ejecuta el código hasta que se alcanza el retorno, luego devuelve el valor final

Crea un iterador generador

Terminación

Terminado por la declaración de retorno

Pausado por rendimiento, puede reanudarse más tarde

Valor de retorno

Objeto único (puede ser una estructura de datos)

Generador iterador

Expresión del rendimiento

No procede (crea una declaración)

Se evalúa a Ninguno o al valor enviado mediante .send()

Casos prácticos

Ideal para devolver un resultado final

Ideal para crear un flujo de datos, especialmente secuencias grandes o infinitas

Conclusión

La palabra clave yield de Python se utiliza en las funciones para definir una función generadora. Cuando se llaman, estas funciones crean iteradores generadores. Los generadores son un ejemplo de evaluación perezosa en Python, donde las expresiones se evalúan cuando se necesita el valor en lugar de cuando se ejecuta la expresión. Por lo tanto, la expresión yield es útil para crear un flujo de datos en el que los valores se generan bajo demanda sin necesidad de almacenarlos en memoria.

Las consideraciones de eficiencia son importantes cuando se trata de grandes conjuntos de datos que requieren muchas operaciones. Los iteradores generadores de Python son una de las principales herramientas necesarias para manipular grandes cantidades de datos con eficacia.

Si quieres aprender más sobre Python, consulta este Trayectoria profesional de Desarrollador Python.

Temas

Aprende Python con estos cursos

curso

Introduction to Python for Developers

3 hr
13.1K
Master the fundamentals of programming in Python. No prior knowledge required!
Ver detallesRight Arrow
Comienza El Curso
Ver másRight Arrow
Relacionado
Python snake

blog

¿Para qué se utiliza Python? 7 usos reales de Python

¿Te has preguntado alguna vez para qué se utiliza Python en el mundo real? Echa un vistazo a 7 usos prácticos de este potente lenguaje de programación.

Elena Kosourova

11 min

tutorial

Tutorial de Iteradores y Generadores de Python

Explore la diferencia entre Iteradores y Generadores de Python y aprenda cuáles son los mejores para usar en diversas situaciones.
Kurtis Pykes 's photo

Kurtis Pykes

10 min

tutorial

Tutorial de comprensión del diccionario Python

¡Aprende todo sobre la comprensión de diccionarios en Python: cómo puedes utilizarla para crear diccionarios, para sustituir los for loops (anidados) o las funciones lambda por map(), filter() y reduce(), ...!
Sejal Jaiswal's photo

Sejal Jaiswal

14 min

tutorial

Tutorial de funciones de Python

Un tutorial sobre funciones en Python que cubre cómo escribir funciones, cómo invocarlas y mucho más.
Karlijn Willems's photo

Karlijn Willems

14 min

tutorial

Función Print() de Python

Aprenda cómo puede aprovechar la capacidad de una simple función de impresión de Python de varias maneras con la ayuda de ejemplos.
Aditya Sharma's photo

Aditya Sharma

10 min

tutorial

Tutorial de Generación de nubes de palabras en Python

Aprende a realizar Análisis exploratorios de datos para el Procesamiento del lenguaje natural utilizando WordCloud en Python.
Duong Vu's photo

Duong Vu

21 min

See MoreSee More