From c662bb288b2072fcc4b6907163e4da163babb71b Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Tue, 11 Jul 2023 20:45:51 +0200 Subject: Clarify Iterable vs Iterator --- es/05_oop.md | 66 ++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 17 deletions(-) (limited to 'es/05_oop.md') diff --git a/es/05_oop.md b/es/05_oop.md index 99e09d1..81fa3af 100644 --- a/es/05_oop.md +++ b/es/05_oop.md @@ -615,9 +615,10 @@ Igual que en el caso de `__len__`, que servía para habilitar la llamada a la función `len`, `__iter__` y `__next__` sirven, respectivamente, para habilitar las llamadas a `iter` y `next`. -La función `iter` sirve para convertir el elemento a *iterable*, que es una -clase que soporte el funcionamiento de la función `next`. Y `next` sirve para -pasar al siguiente elemento de un iterable. Ejemplificado: +La función `iter` sirve para convertir obtener un *iterador* de un *iterable*. +Este *iterador* es un objeto que soporta el funcionamiento de la función +`next`. Y `next` sirve para pasar al siguiente elemento de la iteración. +Ejemplificado: ``` python >>> l = [1,2,3] @@ -645,41 +646,58 @@ La función `__next__` tiene un comportamiento muy sencillo. Si hay un próximo elemento, lo devuelve. Si no lo hay lanza la excepción `StopIteration`, para que la capa superior la capture. -Fíjate que la lista por defecto no es un iterable y que se debe construir un -elemento iterable desde ella con `iter` para poder hacer `next`. Esto se debe a -que la función `iter` está pensada para restaurar la posición del cursor en el -primer elemento y poder volver a iniciar la iteración. +Fíjate que la lista, que es un elemento sobre el que se puede iterar, esto es, +un *iterable*, no soporta `next` sino que necesita obtener un *iterador* a +partir de ella mediante `iter` y es éste el que soporta el `next`. Esto se debe +a que la función `iter` está pensada para restaurar la posición del cursor en +el primer elemento y poder volver a iniciar la iteración. -Sorprendentemente, este es el procedimiento de cualquier `for` en python. El -`for` es una estructura creada sobre un `while` que construye iterables e -itera sobre ellos automáticamente. +Sorprendentemente, éste es el procedimiento de cualquier `for` en python. El +`for` es una estructura creada sobre un `while` que obtiene el *iterador* e +itera sobre él automáticamente. Este bucle `for`: ``` python -for el in secuencia: +for el in iterable: # hace algo con `el` ``` Realmente se implementa de la siguiente manera: ``` python -# Construye un iterable desde la secuencia -iter_obj = iter(secuencia) +# Construye un iterador desde la secuencia +iterador = iter(secuencia) # Bucle infinito que se rompe cuando `next` lanza una # excepción de tipo `StopIteration` while True: try: - el = next(iter_obj) + el = next(iterador) # hace algo con `el` except StopIteration: break ``` Así que, si necesitas una clase con capacidad para iterarse sobre ella, puedes -crear un pequeño iterable que soporte el método `__next__` y devolver una -instancia nueva de éste en el método `__iter__`. +crear un pequeño *iterador* que soporte el método `__next__` y devolver una +instancia nueva de éste en el método `__iter__` de tu clase. + +La diferencia entre el *iterable* y el *iterador* es importante: el *iterable* +es un objeto sobre el que se puede iterar, y el *iterador* el objeto que se +utiliza para iterar sobre el *iterable*. Es decir, el *iterador* es la +herramienta que se usa para iterar sobre algo sobre lo que se puede iterar (un +*iterable*). Esto permite separar conceptos de forma clara, permitiendo, como +se introdujo antes, reiniciar el iterador cada vez que se crea un bucle nuevo, +pero también permitiendo asignar diferentes modos de iteración para el mismo +tipo de objeto. Imagina, por ejemplo, in iterador que salta los elementos +impares de una colección. + +En la práctica, en casos sencillos, el propio *iterable* implementa la interfaz +de su *iterador* y se devuelve a sí mismo en el método `__iter__`. De esta +manera el programador no necesita escribir dos clases y se puede salir con la +suya haciendo la mitad del trabajo, pero la realidad es que es interesante +separar los conceptos, sobre todo para el caso más general. ### *Inicializable* @@ -1026,6 +1044,20 @@ for i in it: print(i) ``` +> Esta clase implementa tanto el iterable como su propio iterador, pero fíjate +> que cuando se llama a `__iter__` reinicia la cuenta. Otra forma de hacer esto +> podría ser con dos clases que diferencien bien los conceptos (*iterable* e +> *iterador*), pero es quizás demasiado escribir para un caso tan sencillo. +> +> El método `__repr__` hace mucho uso de la concatenación de strings y de la +> conversión de valores a string. Históricamente en python siempre ha habido +> formas más elegantes de hacer esto. La más reciente (a partir de python 3.6) +> es utilizar lo que se conoce como *formatted string literals* o *f-strings*. +> Puedes leer más sobre ellos en el PEP 498[^pep498] + +[^pep498]: Puedes leer el contenido completo del PEP en: + + #### Ejercicio libre: generadores La parte de la iteración del ejemplo previo puede realizarse forma más breve @@ -1041,7 +1073,7 @@ propia sentencia `yield`. from datetime import datetime, timedelta def iterate_dates( date_start, date_end=datetime.today(), - separator='/', step=timedelta(days=1)): + separator='/', step=timedelta(days=1) ): date = date_start while date < date_end: yield date.strftime('%Y'+separator+'%m'+separator+'%d') -- cgit v1.2.3