ITERATORS



      Los objetos iterator de Python se vinculan de manera directa con los objetos generator que acabamos de estudiar en la página anterior y comparten un mismo ecosistema de trabajo. Realmente, no son imprescindibles para codificar y para nuestras codificaciones más complejas podemos prescindir tranquilamente de ellos sin que se nos desdibuje el flequillo. Pero bien es verdad que son mecanismos  que nos facultan para extraer un provecho mayor del lenguaje y, además, codificar de una forma más eficiente y elegante, en la órbita estilística del zen de Python.
Al tajo.
Vamos a hacer un poquito de memoria y repasar por encima lo que es un ITERABLE para Python tomando como base los objetos de tipo lista.
Ejemplos de cómo construir listas y manipularlas, por ejemplo, mostrando todos sus elementos:


Y ¿cómo no?, la función conversora list() sobre una secuencia en otro formato (tupla, set, ...):


Como ya sabemos, los tipos de datos que Python considera ITERABLES (y repasemos una vez más la excelente definición de Eugenia Bahit sobre los mismos: Una secuencia de datos que se lee de izquierda a derecha donde cada elemento es susceptible de ser leído por un bucle for/in) son:
  • STRINGS (cadenas de caracteres literales)
  • LISTAS
  • TUPLAS 
  • DICCIONARIOS
  • SET (conjuntos. Es un iterable a su manera pues, si bien puede ser recorrido por un bucle for/in, la aleatoriedad intrínseca en el manejo de sus ítems constituyentes no lo hacen para nada recomendable usarlos como tal)

Podemos emplear aquí otra definición de iterable como cualquier objeto de Python susceptible de recibir el método especial __iter__() (ya veremos en la Programación Orientada a Objetos qué es esto tan bonito de los métodos especiales):


Esta condición intrínseca de iterabilidad proporcionada por el método especial __iter__() que se asigna a cualquier tipo de dato susceptible de ser recorrido lindamente por un bucle for/in, es lo que nos permite hacer cosas tan chulas como ésta:


Las iteraciones en Python son posibles porque los dos componentes internos del lenguaje, esto es, el intérprete (traductor código máquina lenguaje "humano") y el código interno (código máquina), basado en C, en que se construye Python cuentan con un protocolo de actuación común frente a estructuras de datos que poseen esta capacidad para implementarlos en nuestros scripts y, a partir de aquí, aplicarles las funcionalidades reservadas a los tipos de datos iterables.
Situándonos a bajo nivel, lo que realmente sucede cuando empleamos el bucle for/in, es que el código máquina evalúa la iterabilidad del dato que acabamos de codificar e invoca posteriormente a un iterador preexistente en los intestinos de Python (objeto iterator) para llevar a cabo la dichosa iteración sobre el dato, siempre y cuando la mencionada evaluación devenga True.




EXTREMO NOROCCIDENTAL DEL MACIZO DE TENO, DESDE LAS ALTURAS, ENTRE CARDONES Y TABAIBAS, CON LA GRAN EXPLANADA DE LAVA Y CULTIVOS EN EL CUADRANTE INFERIOR DERECHO Y, EN EL CENTRO, SOBRE UN ESPIGÓN DE ROCA QUE SE ADENTRA EN EL MAR, EL FARO DE TENO. AL FONDO, LA SILUETA DE LA ISLA DE LA GOMERA. NOROESTE DE TENERIFE
En resumidas cuentas, el objeto iterator opera a nivel de código máquina que, a su vez, suministra el método especial __next__() del mismo modo que suministra también el método especial __iter__() con el que evalúa previamente la iterabilidad del dato. Si recordamos lo ya visto en el capítulo dedicado a los generators, en ellos contábamos con la función next(iterator) que funciona de manera unitaria actuando elemento por elemento que, a su vez, mire usted por dónde, es el protocolo de ejecución en que se basa el bucle for/in, leyendo cada elemento del iterable de uno en uno. 
El objeto iterator lanzará una excepción StopIteration en el momento en que alcanza el extremo derecho del iterable y no encuentra más elementos. Recordemos que estas llamadas elemento por elemento, detención del proceso, evaluación, prosecución del proceso llamando al siguiente elemento (o lanzando StopIteration) es el alma de la declaración yield en las funciones generators del capítulo anterior.


      EL ORDEN EN QUE SE DEVUELVE EL RESULTADO DE LA ITERACIÓN, EN NUESTRO EJEMPLO EN EL ESQUEMA, 1, 2, 3, 4, 5, DEPENDE DEL ITERABLE: SI ES UNA lista O UNA tupla, LOS ELEMENTOS SE SUELEN DEVOLVER EN ORDEN SECUENCIAL, COMENZANDO POR EL ÍTEM QUE TENGA UN VALOR DE ÍNDICE 0 (index = 0) Y DE AQUÍ EN ADELANTE; SI SE TRATA DE UN diccionario O DE UN conjunto, NUESTRO ITERADOR PUEDE DEVOLVER EL RESULTADO DE LA ITERACIÓN SOBRE AMBOS TIPOS DE DATOS DE FORMA ALEATORIA...SNIFF, SNIFF,..¡GALLETAS! ...ADIÓS, ME VOY.



La función integrada iter() que como nos podemos imaginar nos puede servir para saber si el tipo de dato almacenado en una variable es iterable o no, presenta dos comportamientos asimétricos:




  1. Cuando en el paso de parámetros introducimos como argumento una secuencia, ésta nos devuelve un objeto iterator (<list_iterator object at0x0000000003006898>) específico para el argumento (en el ejemplo superior, para la lista almacenada con el referente de variable a). En el supuesto de que la variable que le pasamos como argumento a la función no guardara una secuencia, se lanzaría una excepción de tipo TypeError como también podemos comprobar en el ejemplo.
  2. En el caso de que en el paso de parámetros introdujéramos como argumento un objeto callable (veremos con algo más de sustancia qué es un objeto callable en la entrada T4. CALLABLE: SI TÚ ME DICES 'VEN'...), normalmente una función/método o una instancia de clase, esto último, en la POO, así que, de momento, no le demos mayor importancia, y que en consecuencia pueden ser llamados o invocados (to call y, de aquí, 'call' + 'able' ("capaz"), traducido, "capaz de ser llamado") junto a un valor que recibe (en ocasiones) el nombre de sentinel, "centinela" y que actúa como una suerte de limitador en el número de iteraciones sobre una secuencia dada, la función iter(secuencia) llamará al objeto callable (callable object) una sóla vez por cada una de las vueltas de bucle que genere el loop for/in, devolviendo en cada ocasión el valor de retorno (return) de la función. En el momento mágico e irrepetible en que el valor de retorno y el valor "centinela" coincidan (retorno = = "centinela"), se detendrá la iteración y se lanzará la archiconocida excepción StopIteration.


Cuando utilizamos un bucle for/in, Python invoca, a nivel de código máquina,  como podemos ver en el esquema superior, a la función iter(secuencia) para obtener el objeto iterator correspondiente. A continuación se invoca al método especial  __next__() para este objeto iterator en cada una de las vueltas del bucle para conseguir el elemento siguiente hasta el momento en que se produzca la excepción StopIteration, bien porque actúe el valor "centinela" demarcando el límite de iteraciones, o bien porque se haya alcanzado el último elemento de la secuencia y ya no fuera posible iterar más, en cuyo caso el bucle concluirá su ejecución.

      OTRA FORMA DE OBTENER EL SIGUIENTE ELEMENTO DE UN ITERABLE PARA SER PROCESADO POR UN ITERADOR ES INVOCAR A LA FUNCIÓN INTEGRADA next(). EN EL EJEMPLO SIGUIENTE SE MUESTRAN DOS CÓDIGOS EQUIVALENTES (QUE MULTIPLICAN LOS VALORES DE UNA LISTA), UNO CON UN BUCLE for/in Y EL OTRO CON UN ITERADOR EXPLÍCITO MÁS UN TERCERO CON EL MÉTODO ESPECIAL __iter__().



1. Creamos una variable i que almacena a la función iter(secuencia), y que llevará como argumento a una secuencia iterable, en este caso, a un objeto lista.
2. Establecemos en esta ocasión (para variar) un bucle dinámico while al que asignamos como valor el booleano True, "mientras se cumpla la condición dada", que será el encargado de efectuar las iteraciones.
3. Probamos con un bloque try/except el código del que queremos cazar la excepción StopIteration.
4. La variable product será multiplicada cada vez por el valor que vaya asumiendo cada vez next(i), es decir, por cada elemento del iterable i. Como el iterable i contiene cuatro elementos len(i) == 4, la función next(i) será invocada cinco veces ((len(i) == 4) + 1), esto es, una vuelta por cada uno de los elementos de la lista más una vuelta más que será la encargada de lanzar la excepción StopIteration, que es la excepción que queremos capturar (5.) cuando la función iter(secuencia) no encuentre ningún elemento más sobre el que iterar.
6. Cuando se produzca la excepción StopIteration se romperá, break, el bucle dinámico while y salimos del mismo, imprimiéndose el resultado obtenido hasta entonces (7.).


      DEJAD QUE OS LADRE UNA COSITA: TENGAMOS EN CUENTA QUE TANTO LA FUNCIÓN iter(secuencia) COMO LA FUNCIÓN next(iterator) SON FUNCIONES built-in QUE PODEMOS INVOCAR A ALTO NIVEL, MIENTRAS QUE LOS MÉTODOS ESPECIALES __iter__() Y __next__(), QUE REALIZAN LA MISMA FUNCIÓN, ACTÚAN A BAJO NIVEL, ESTO ES, EN CÓDIGO MÁQUINA (Y ESTO ES VÁLIDO PARA TODOS LOS MÉTODOS ESPECIALES) Y SE ASIGNAN AL OBJETO ITERABLE POR EL MÉTODO DEL PUNTO (EN NUESTRO EJEMPLO, a.__iter__()). HABLAREMOS DE LOS MÉTODOS ESPECIALES CUANDO ESTUDIEMOS LA PROGRAMACIÓN ORIENTADA A OBJETOS JUNTO AL CONCEPTO DE 'SOBRECARGA' CON EL QUE SE RELACIONA DIRECTAMENTE .


La última de las funciones mostradas en el ejemplo anterior constituye una tercera alternativa a los dos casos anteriores, esta vez, con el concurso del método especial iter(secuencia). Como hemos visto, para construir un objeto así tenemos, OBLIGATORIAMENTE, que implementar los métodos 
iter(secuencia) y next(iterator) del modo siguiente:





Dicho de otro modo, iter(secuencia) engloba al iterable (o a la referencia que lo apunta, para ser exactos), y next(iterator) aplica la funcionalidad desenvolviendo la iteración cada vez sobre cada elemento.




MONTE HÚMEDO (LAURISILVA) DE LA CUMBRILLA, MACIZO DE ANAGA, NORESTE DE TENERIFE.

Aunque todavía no toca (paciencia, ya queda poquito) veamos varios ejemplos de funcionamiento de los métodos especiales __iter__() y __next__() en el ámbito de las CLASES (Programación Orientada a Objetos, POO, por sus siglas en español) con el propósito de quedarnos con cierta idea del asunto. En este ámbito tenemos que __iter__() se utiliza para devolver una referencia a la instancia de la clase que implementa el iterador (iterator), mientras que __next__() implementa la funcionalidad, que se ejecutará cada vez conforme ocurra una vuelta de bucle elemento por elemento de la secuencia.
Imaginemos que queremos implementar una "función" que cuente hacia atrás. Nuestra CLASE dispondrá de una cualidad iteradora en tanto en cuenta acoge en su ámbito las funciones __iter__() y __next__() del modo que se muestra:


1. El constructor de la CLASE, __init__(self. inicio), recibirá un valor inicial: inicio. Esto se realiza a través de una función definida por el usuario, def, que determinará una serie de comportamientos: en este primero, construye/inicializa la CLASE y, en los dos siguientes, con __iter__(self) crea un objeto iterator y, con __next__(self) itera de uno en uno cada vez sobre el objeto iterator creado en la función definida anterior. La palabra self (lo trataremos como debe ser en su momento, insistimos) es una suerte de autorreferencia a la propia CLASE de uso obligatorio cuando trabajamos con CLASES.
2. Aquí se crea una instancia de clase con el nombre contador. Se utiliza la sintaxis del punto junto a la palabra self que apunta a la CLASE Reduce para decir que este objeto contador, esta instancia, pertenece a esta CLASE Reduce y no a otra, estableciendo el vínculo necesario entre una CLASE y cualquier objeto que nazca de ella.
3. Llamamos al método especial __iter__(self),  que como acabamos de aprender (v. esquema) debe siempre preceder al método __next__(self), a través de la función def __iter__(self):. Nótese que los nombres de las funciones def se corresponden directamente con los métodos especiales en sí. Esto, aunque nos pueda descolocar un poco, es perfectamente posible y factible en Python.
4. Como no partimos de ninguna secuencia preconstruida sobre el que iterar, dado que ésta se generará de forma dinámica, llamamos a la cláusula return para cerrar la función como Dios manda, y le pasamos la autorreferencia self. Con esto ya tenemos el objeto iterator que generará el método especial __iter__(self) y podremos invocar al método especial __next__(self) a través de la tercera y última función definida por el usuario.
5. Cuando se vaya a ejecutar la funcionalidad de __next__(self), éste comprobará primero si el valor de self.contador es menor o igual a 0.
6. Y si fuera así, se detiene el proceso lanzando la excepción StopIteration. En el caso de que la condición declarada en 6. no se cumpliera, pasamos a...
7. ... donde declaramos una variable nueva que llamamos rsltdo, y a la que asignamos el valor actual de self.contador, esto es, del elemento de la secuencia sobre el que recae en ese mismo momento la iteración.
8. Introducimos la operación aritmética sobre self.contador que permite el decrecimiento con cada valor que va asumiendo la variable iniciodef __init__(self, inicio): con cada vuelta del bucle (no sobre rsltdo, porque su finalidad, su razón de ser, es la de mostrarnos un resultado en pantalla).
9. Retorna el resultado actual de la ejecución de __next__(self), es decir, rsltdo, y reejecuta la acción nuevo bucle de __next__(self) hasta que rsltdo = 0 y se lance la excepción StopIteration.
Para ver en acción nuestra CLASE Reduce (es decir, nuestro iterador) tenemos que echar mano del bucle for/in... et voilà.

Este script muestra que se trata de una buena idea construir ("modelizar" es su nombre adecuado, pero esto lo veremos en su momento) una clase específica, ad hoc, cuando queremos construir una iteración dinámica, con elementos que se van añadiendo a un iterable que se va configurando sobre la marcha, esto es, no declarada de antemano, recurriendo a los métodos especiales __iter__(self)  y  __next__(self).
Por otro lado, resulta del todo factible trabajar con más de un argumento en el constructor, por ejemplo, si necesitamos iterar sobre un rango de números concreto:




      LAS FUNCIONES INTEGRADAS range(), QUE YA CONOCEMOS EN EJEMPLOS VARIOS  SOBRE ITERACIONES CON EL BUCLE for/in ASÍ COMO LAS FUNCIONES DE ORDEN SUPERIOR filter(), map(), (https://conocepython.blogspot.com/p/programacion-funcional.html) Y LA SENCILLA, VERSÁTIL Y MUY EFICAZ FUNCIÓN zip() (http://conocepython.blogspot.com/p/funciones-exec.html) DEVUELVEN TODAS ELLAS OBJETOS iterators. 


Terminamos con un ejemplo final de cómo funciona una función iter(iterable, sentinel) con su "centinela":


Como lo prometido es deuda, vamos con la entrada anunciada más arriba: T4. CALLABLE: SI TÚ ME DICES 'VEN'...

CASA RURAL REMOZADA EN LAS PROXIMIDADES DEL CAMPO DE IFONCHE, ALTOS DE VILAFLOR, CON HIGUERAS, HIEDRAS SOBRE LA PARED DE CANTO (BLOQUE DE PUMITA, PIEDRA VOLCÁNICA, TÍPICA DE ALGUNAS CONSTRUCCIONES RURALES DEL SUR DE TENERIFE), UN PRECIOSO CASTAÑO A LA IZQUIERDA Y LAS SEMPITERNAS TABAIBAS. DETRÁS, PINOS Y NIEBLA EN EL CENTRO SUR DE TENERIFE.


No hay comentarios:

Publicar un comentario