Un generator es una función o expresión que devuelve un tipo especial de iterador llamado generator iterator.
Los Generator-iterator son perezosos: no almacenan sus values en memoria, sino que generan sus valores cuando se necesitan.
Una función generadora es como cualquier otra función, pero contiene una o más expresiones yield.
Cada yield suspende la ejecución del código y guarda el estado de ejecución actual (incluidas todas las variables locales y las instrucciones try).
Cuando el generador se reanuda, retoma el estado desde la suspensión, a diferencia de las funciones normales, que se reinician en cada llamada.
Los generadores se construyen de forma muy similar a otras funciones con bucles o recursivas, pero requieren una expresión yield, que exploraremos en profundidad un poco más adelante.
Un ejemplo es una función que devuelve los cuadrados de un array de números dado. Tal como está escrita, hay que procesar toda la entrada antes de poder devolver algún valor:
>>> def squares(list_of_numbers):
... squares = []
... for number in list_of_numbers:
... squares.append(number ** 2)
... return squares
Puedes convertir esa función en un generador así:
>>> def squares_generator(list_of_numbers):
... for number in list_of_numbers:
... yield number ** 2
La razón es que usas un generador cuando no necesitas producir todos los valores a la vez. Esto ahorra memoria y potencia de procesamiento, ya que solo se calcula el valor en el que estás trabajando en ese momento.
Los generadores pueden usarse en lugar de la mayoría de los iterables de Python.
Esto incluye funciones u objetos que requieren un iterable/iterator como argumento.
Para usar el generador squares_generator():
>>> squared_numbers = squares_generator([1, 2, 3, 4])
>>> for square in squared_numbers:
... print(square)
...
1
4
9
16
Los valores dentro de un generator también pueden producirse o consultarse mediante la función next().
next() llama al método __next__() de un objeto generador-iterador, «avanzando» o evaluando el código hasta su expresión yield, que entonces «cede» o devuelve un valor:
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
Cuando un generator-iterator se consume por completo y no le quedan valores que devolver, lanza un error StopIteration.
>>> next(squared_numbers)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Los generadores-iteradores son un subconjunto especial de los iteradores.
Los Iterators son el mecanismo o protocolo que permite iterar sobre iterables.
Los generadores-iteradores y los iteradores que devuelven los iterables habituales de Python funcionan de forma muy similar, pero hay algunas diferencias importantes que conviene tener en cuenta:
indexes.
No puedes referenciar un valor anterior o futuro mediante sumas o restas, ni usar la notación de corchetes ([]) ni el slicing.len(), ya que no tienen longitud.generator-iterator infinito!La expresión yield es muy similar a la expresión return.
A diferencia de la expresión return, yield cede valores a quien lo llama en un punto concreto, suspendiendo la evaluación o la devolución de cualquier valor adicional hasta que se soliciten.
Cuando se evalúa yield, se pausa la ejecución de la función que lo contiene y se devuelven los valores de la función en ese momento.
La función entonces permanece en el scope, y cuando se llama a __next__(), la ejecución se reanuda hasta que se encuentra yield de nuevo.
El uso de expresiones yield está prohibido fuera de las funciones.
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
>>> lets_try = infinite_sequence()
>>> lets_try.__next__()
0
>>> lets_try.__next__()
1
Los generadores son útiles en muchas aplicaciones.
Cuando trabajas con una colección de valores potencialmente grande, puede que no quieras cargarlos todos en memoria. Puedes usar un generador para trabajar con datos más grandes pieza a pieza, ahorrando memoria y mejorando el rendimiento.
Los generadores también resultan muy útiles cuando un proceso o cálculo es complejo, costoso o infinito:
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
Ahora, siempre que se llame a __next__() en el objeto infinite_sequence, devolverá el número anterior + 1.
Conda Airlines es la aerolínea más grande del mundo de la programación, ¡con más de 10.000 vuelos al día!
Actualmente asignan todos los asientos a los pasajeros a mano; esto tendrá que automatizarse.
Te han pedido a ti que crees un software para automatizar la asignación de asientos a los pasajeros. Necesitan que tu software sea eficiente en memoria y en rendimiento.
Conda quiere generar las letras de asiento de sus aviones.
Un avión está formado por filas de asientos.
Cada fila tiene 4 asientos.
Los asientos de cada fila siempre se llaman A, B, C y D.
El primer asiento de la fila es A, el segundo asiento de la fila es B, y así sucesivamente.
Tras llegar a D, debe empezar de nuevo por A.
Implementa una función generate_seat_letters(<number>) que acepte un int con cuántas letras de asiento hay que generar.
La función debe entonces devolver un iterable de letras de asiento.
>>> letters = generate_seat_letters(4)
>>> next(letters)
"A"
>>> next(letters)
"B"
Conda quiere un sistema capaz de generar un número determinado de asientos para sus aviones.
Cada avión tiene 4 asientos en cada fila.
Las filas se definen con números, empezando por 1 y subiendo.
Los asientos deben ordenarse así: 1A, 1B, 1C, 1D, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, ...
Aquí tienes un ejemplo:
| x | 1 | 2 |
|---|---|---|
| Fila | 5 | 21 |
| Letra del asiento | A | D |
| Resultado | 5A | 21D |
Muchas aerolíneas no tienen la fila número 13 en sus vuelos, debido a las supersticiones de los pasajeros. Conda Airlines también sigue esta convención, así que asegúrate de no generar asientos para la fila número 13.
Implementa una función generate_seats(<number>) que acepte un int con cuántos asientos hay que generar.
La función debe entonces devolver un iterable de los asientos generados.
>>> seats = generate_seats(10)
>>> next(seats)
"1A"
>>> next(seats)
"1B"
Ahora que ya tienes una función que genera asientos, puedes usarla para asignar asientos a los pasajeros.
Implementa una función assign_seats(<passengers>) que acepte una list con los nombres de los pasajeros.
La función debe entonces devolver un diccionario con el passenger como clave y el seat_number como valor.
>>> passengers = ['Jerimiah', 'Eric', 'Bethany', 'Byte', 'SqueekyBoots', 'Bob']
>>> assign_seats(passengers)
{'Jerimiah': '1A', 'Eric': '1B', 'Bethany': '1C', 'Byte': '1D', 'SqueekyBoots': '2A', 'Bob': '2B'}
A Conda Airlines le gustaría tener un código único para cada billete. Como es una aerolínea grande, tiene muchos vuelos. Esto significa que hay varios vuelos con el mismo número de asiento. Quieren que crees un sistema que genere un billete único que sea un string de 12 caracteres.
Este código empieza por el assigned_seat seguido del flight_id.
El resto del código se rellena con 0s.
Implementa una función generate_codes(<seat_numbers>, <flight_id>) que acepte una list de seat_numbers y un string con el número de vuelo.
La función debe entonces devolver un generator que genere un ticket_number.
>>> seat_numbers = ['1A', '17D']
>>> flight_id = 'CO1234'
>>> ticket_ids = generate_codes(seat_numbers, flight_id)
>>> next(ticket_ids)
'1ACO12340000'
>>> next(ticket_ids)
'17DCO1234000'
Regístrate en Exercism para aprender y dominar Python con 17 conceptos146 ejercicios y mentoría humana real, todo gratis.