Iteradores fantásticos y cómo hacerlos

Foto de John Matychuk en Unsplash

El problema

Mientras aprendía en Make School, he visto a mis compañeros escribir funciones que crean listas de elementos.

s = 'baacabcaab'
p = 'a'
def find_char (cadena, caracter):
  índices = list ()
  para index, str_char en enumerate (string):
    si str_char == carácter:
      indices.append (index)
  índices de retorno
print (find_char (s, p)) # [1, 2, 4, 7, 8]

Esta implementación funciona, pero plantea algunos problemas:

  • ¿Qué pasa si solo queremos el primer resultado; ¿Necesitaremos hacer una función completamente nueva?
  • ¿Qué pasa si todo lo que hacemos es recorrer el resultado una vez? ¿Necesitamos almacenar cada elemento en la memoria?

Los iteradores son la solución ideal para estos problemas. Funcionan como "listas perezosas" en el sentido de que en lugar de devolver una lista con cada valor que produce y devuelve cada elemento de uno en uno.

Los iteradores perezosamente devuelven valores; Ahorro de memoria.

¡Así que vamos a sumergirnos en aprender sobre ellos!

Iteradores integrados

Los iteradores que son más frecuentes son enumerate () y zip (). Ambos devuelven perezosamente los valores de next () con ellos.

range (), sin embargo, no es un iterador, sino un "vago iterable". Explicación

Podemos convertir range () en un iterador con iter (), por lo que lo haremos por nuestros ejemplos en aras del aprendizaje.

my_iter = iter (rango (10))
print (siguiente (my_iter)) # 0
print (next (my_iter)) # 1

En cada llamada de next () obtenemos el siguiente valor en nuestro rango; tiene sentido ¿verdad? Si desea convertir un iterador en una lista, simplemente dele el constructor de la lista.

my_iter = iter (rango (10))
print (list (my_iter)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Si imitamos este comportamiento, comenzaremos a comprender más sobre cómo funcionan los iteradores.

my_iter = iter (rango (10))
my_list = list ()
tratar:
  mientras cierto:
    my_list.append (next (my_iter))
excepto StopIteration:
  pasar
print (mi_lista) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Puede ver que necesitábamos envolverlo en una declaración try catch. Esto se debe a que los iteradores aumentan StopIteration cuando se han agotado.

Entonces, si llamamos a continuación en nuestro iterador de rango agotado, obtendremos ese error.

next (my_iter) # Raises: StopIteration

Hacer un iterador

Intentemos crear un iterador que se comporte como un rango con solo el argumento de detención mediante el uso de tres tipos comunes de iteradores: clases, funciones de generador (rendimiento) y expresiones de generador

Clase

La antigua forma de crear un iterador era a través de una clase explícitamente definida. Para que un objeto sea un iterador, debe implementar __iter __ () que se devuelve y __next __ () que devuelve el siguiente valor.

clase my_range:
  _corriente = -1
  def __init __ (self, stop):
    self._stop = stop
  def __iter __ (self):
    volver a sí mismo
  def __next __ (self):
    self._current + = 1
    if self._current> = self._stop:
      elevar StopIteration
    return self._current
r = mi_rango (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Eso no fue demasiado difícil, pero desafortunadamente, tenemos que hacer un seguimiento de las variables entre las llamadas de next (). Personalmente, no me gusta la placa repetitiva o cambiar mi forma de pensar sobre los bucles porque no es una solución directa, por lo que prefiero los generadores

El principal beneficio es que podemos agregar funciones adicionales que modifican sus variables internas como _stop o crear nuevos iteradores.

Los iteradores de clase tienen el inconveniente de necesitar repetitivo, sin embargo, pueden tener funciones adicionales que modifican el estado.

Generadores

PEP 255 introdujo "generadores simples" utilizando la palabra clave de rendimiento.

Hoy, los generadores son iteradores que son más fáciles de hacer que sus contrapartes de clase.

Función de generador

Las funciones del generador son lo que finalmente se discutió en ese PEP y son mi tipo favorito de iterador, así que comencemos con eso.

def my_range (detener):
  índice = 0
  mientras index 
r = mi_rango (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

¿Ves lo hermosas que son esas 4 líneas de código? ¡Es ligeramente más corto que nuestra implementación de lista para completarlo!

El generador funciona con iteradores con menos repetitivo que las clases con un flujo lógico normal.

El generador funciona automáticamente "pausa" la ejecución y devuelve el valor especificado con cada llamada de next (). Esto significa que no se ejecuta ningún código hasta la primera llamada next ().

Esto significa que el flujo es así:

  1. se llama a next (),
  2. El código se ejecuta hasta la siguiente declaración de rendimiento.
  3. Se devuelve el valor a la derecha del rendimiento.
  4. La ejecución está en pausa.
  5. 1–5 repita para cada llamada siguiente () hasta que se toque la última línea de código.
  6. StopIteration se eleva.

Las funciones del generador también le permiten utilizar el rendimiento de la palabra clave que future next () llama a otro iterable hasta que dicho iterable se haya agotado.

def yielded_range ():
  rendimiento de my_range (10)
print (list (yielded_range ())) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Ese no fue un ejemplo particularmente complejo. ¡Pero incluso puedes hacerlo de forma recursiva!

def my_range_recursive (stop, current = 0):
  si actual> = detener:
    regreso
  rendimiento actual
  rendimiento de my_range_recursive (stop, current + 1)
r = my_range_recursive (10)
print (list (r)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Expresión de generador

Las expresiones generadoras nos permiten crear iteradores como frases simples y son buenas cuando no necesitamos darle funciones externas. Desafortunadamente, no podemos hacer otro my_range usando una expresión, pero podemos trabajar en iterables como nuestra última función my_range.

my_doubled_range_10 = (x * 2 para x en my_range (10))
print (list (my_doubled_range_10)) # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Lo bueno de esto es que hace lo siguiente:

  1. La lista le pregunta a my_doubled_range_10 su próximo valor.
  2. my_doubled_range_10 le pregunta a my_range su próximo valor.
  3. my_doubled_range_10 devuelve el valor de my_range multiplicado por 2.
  4. La lista agrega el valor a sí misma.
  5. 1–5 repita hasta que my_doubled_range_10 aumente StopIteration que ocurre cuando my_range lo hace.
  6. Se devuelve la lista que contiene cada valor devuelto por my_doubled_range.

¡Incluso podemos hacer filtros usando expresiones generadoras!

my_even_range_10 = (x para x en my_range (10) si x% 2 == 0)
print (list (my_even_range_10)) # [0, 2, 4, 6, 8]

Esto es muy similar al anterior, excepto que my_even_range_10 solo devuelve valores que coinciden con la condición dada, por lo que solo valores pares entre el rango [0, 10).

A lo largo de todo esto, solo creamos una lista porque se lo pedimos.

El beneficio

Fuente

Como los generadores son iteradores, los iteradores son iterables, y los iteradores devuelven valores perezosamente. Esto significa que, utilizando este conocimiento, podemos crear objetos que solo nos darán objetos cuando los solicitemos y cuantos deseemos.

Esto significa que podemos pasar generadores a funciones que se reducen entre sí.

print (sum (my_range (10))) # 45

Calcular la suma de esta manera evita crear una lista cuando todo lo que estamos haciendo es sumarlos y luego descartarlos.

¡Podemos reescribir el primer ejemplo para que sea mucho mejor usando una función de generador!

s = 'baacabcaab'
p = 'a'
def find_char (cadena, caracter):
  para index, str_char en enumerate (string):
    si str_char == carácter:
      índice de rendimiento
print (list (find_char (s, p))) # [1, 2, 4, 7, 8]

Ahora, de inmediato, puede que no haya un beneficio obvio, pero vayamos a mi primera pregunta: "¿qué pasa si solo queremos el primer resultado? ¿necesitaremos hacer una función completamente nueva?

Con una función de generador no necesitamos reescribir tanta lógica.
print (next (find_char (s, p))) # 1

Ahora podríamos recuperar el primer valor de la lista que dio nuestra solución original, pero de esta manera solo obtenemos la primera coincidencia y dejamos de iterar sobre la lista. El generador será descartado y no se creará nada más; masivo ahorro de memoria.

Conclusión

Si alguna vez está creando una función, los valores se acumulan en una lista como esta.

def foo (bar):
  valores = []
  para x en la barra:
    # alguna lógica
    valores.append (x)
  valores de retorno

Considere hacer que devuelva un iterador con una clase, función generadora o expresión generadora de la siguiente manera:

def foo (bar):
  para x en la barra:
    # alguna lógica
    rendimiento x

Recursos y fuentes

PEP

  • Generadores
  • Generador de Expresiones PEP
  • Rendimiento de PEP

Artículos e hilos

  • Iteradores
  • Iterable vs iterador
  • Documentación del generador
  • Iteradores vs Generadores
  • Generador de Expresión vs Función
  • Generadores Recrusivos

Definiciones

  • Iterable
  • Iterador
  • Generador
  • Generador Iterador
  • Expresión de generador

Publicado originalmente en https://blog.dacio.dev/2019/05/03/python-iterators-and-generators/.