Gracias, siguiente (): familiarizarse con los iteradores de Python
Escribir código, como escribir poesía, literatura o publicaciones de blog, está lleno de opciones estilísticas. Si trabaja con un desarrollador el tiempo suficiente, conocerá estas opciones. El código, como la palabra escrita, requiere el uso de convenciones comunes para cumplir su propósito. En Python, una de las convenciones más importantes, y que a menudo es invisible para nosotros de la misma manera que el orden de las palabras es invisible para nosotros, es el uso de iteradores.
Iteradores que ya conocemos
Si alguna vez ha escrito un bucle for, ha utilizado un iterador, ¡lo sepa o no! Consultando la propuesta de mejora de Python (PEP) que propuso iteradores, PEP 234, un iterador en Python es cualquier objeto que implementa un método __iter__() y un método __next__(). Eso significa que las principales estructuras de datos integradas que usamos en Python: list, dict, set y tuple, son todos objetos iterables: objetos a los que podemos llamar iter() para generar un iterador. Esta es una distinción importante, ya que, por ejemplo, una lista no es en sí misma un iterador, sino que ES un objeto iterable.
>>> my_list = [1, 2, 3, 4]
>>> next(my_list)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator
Sin embargo, si llamamos iter en nuestra lista:
>>> my_list = [1, 2, 3, 4]
>>> my_iterator = iter(my_list)
>>> next(my_iterator)
1
>>> next(my_iterator)
2
Continúe y ejecute el código anterior en su REPL de Python y vea qué sucede cuando llama a next() por quinta vez: ¡lo detiene!
Si bien es posible que no haya utilizado recientemente, o nunca, el método next() de un iterador, probablemente haya utilizado la sintaxis for… in… Cuando escribimos un bucle for, bajo el capó, el intérprete de Python está creando un objeto iterador y llamando al método next() por nosotros.
my_dict = {
"spam": 1,
"ham": 1,
"eggs": 2,
"cheese": 3
}
for k, v in my_dict.items():
print(v, k)
Ejecutar este código nos da el siguiente resultado:
>python example.py
1 spam
1 ham
2 eggs
3 cheese
Ahora, si quisiéramos usar un iterador de nuestro diccionario, podríamos usar un ciclo while como este y obtener el mismo resultado:
my_dict = {
"spam": 1,
"ham": 1,
"eggs": 2,
"cheese": 3
}
dict_iterator = iter(my_dict.items())
i = 0
while i < len(my_dict):
k, v = next(dict_iterator)
print(v, k)
i += 1
Creando nuestro propio iterador
Como sabemos, cualquier objeto con un método __iter__() y un método __next__() es un iterador. También sabemos que llamar a iter() en nuestros objetos iterables nos proporciona un iterador. Pero, ¿y si queremos construir un iterador personalizado? Consideremos el siguiente código.
class Fibonacci():
def __init__(self):
self.value = 1
self.previous = 0
def __iter__(self):
return self
def __next__(self):
current = self.value
previous = self.previous
self.value = current + previous
self.previous = current
return current
¡Este iterador comenzará con 1, y cada llamada a next() devolverá el elemento next() de la secuencia de Fibonacci! (Dejaremos a un lado los argumentos sobre la secuencia que comienza en 0 y reconoceremos que si solo cambiamos el retorno actual para regresar al anterior, obtenemos la secuencia indexada en 0)
Entonces, ¿cómo funciona eso?
iterator = Fibonacci()
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
Dado el objeto de Fibonacci, el código anterior generará la secuencia: 1, 1, 2, 3, 5, 8. Esto se debe a que estamos llamando a next() en nuestro iterador manualmente. ¿Qué sucede si ejecutamos el siguiente código?
iterator = Fibonacci()
for element in iterator:
print(element)
En este caso, dado que no hay límites, el código se ejecutará para siempre (¡o al menos hasta que interrumpa el proceso!). A diferencia del objeto my_iterator de nuestro primer ejemplo que llama a iter() en un objeto de lista, no hay un elemento final de este iterador, por lo que nunca generará una excepción StopIteration. Si queremos hacer esto, simplemente podemos modificar nuestro objeto para que deje de iterar cuando alcance un valor mayor que algún límite superior arbitrario.
class Fibonacci():
def __init__(self, max_val):
self.value = 1
self.previous = 0
self.max_val = max_val
def __iter__(self):
return self
def __next__(self):
if self.value > self.max_val:
raise StopIteration
current = self.value
previous = self.previous
self.value = current + previous
self.previous = current
return current
iterator = Fibonacci(20)
for element in iterator:
print(element)
Ejecutar este código generará: 1, 1, 2, 3, 5, 8, 13 ya que el siguiente valor más grande de la secuencia es 21, que es mayor que nuestro valor máximo especificado de 20.
Conclusión
Los iteradores son una parte increíblemente importante del código de Python. Aunque los iteradores reales a menudo se abstraen de nosotros y, en su lugar, recorremos los objetos iterables, hay momentos en los que querremos o necesitaremos implementar nuestros propios objetos iterables. Es importante para nosotros saber cómo construir nuestros métodos __iter__() y __next__(). Para las aplicaciones de ciencia de datos y aprendizaje automático en grandes conjuntos de datos, se usa con frecuencia un tipo especial de iterador conocido como generador. Si eso te emociona, entonces lo siguiente () que debes hacer es consultar el programa Programación para la ciencia de datos con Python Nanodegree.