FUNCIONES DE ORDEN SUPERIOR

PAISAJE DE LA MESETA ALTA DE TENO ALTO CON EL TEIDE NEVADO AL FONDO, MACIZO DE TENO, NOROESTE DE TENERIFE.
      Debemos saber que entre las innumerables virtudes que adornan a Python se encuentra también el de ser un lenguaje multiparadigmático, esto es, que podemos utilizarlo desde distintos paradigmas de programación. La Programación Orientada a Objetos, POO, por sus siglas en español, que veremos más adelante, es uno de estos paradigmas de programación, el más moderno y entre los más eficaces. Otros paradigmas son, por ejemplo, el Imperativo, de bajo nivel y estrechamente vinculado al código máquina; Dinámica, Dirigida a Eventos o Funcional, entre otros. Y es precisamente de éste último de quien vamos a ver unas pocas funciones integradas en el lenguaje, muy útiles, las más empleadas habitualmente en programación dejando de lado otras un pocas más complejas y de uso mucho más restringido, menos usuales. ¿Y por qué estudiarlas si luego nos centraremos en la POO? Porque Python es un lenguaje, como acabamos de decir, multiparadigma, es decir, que puede implementar en un mismo programa elementos de codificación tanto POO como de otros paradigmas, como el Funcional, consiguiendo con esta combinación de paradigmas mejores resultados, tanto desde el punto de vista de la eficiencia como de la optimización.
¡Que nadie se asuste! ¡Quietos!¡Parados todos!
Las cuatro funciones "funcionales" que vamos a ver son tan sencillas de aprender como lo han sido cualquiera de las anteriores que hemos visto hasta ahora. Veremos que su sintaxis es muy clara, tan despejada como nos tiene acostumbrados Python y, según para qué supuestos en programación, muy, pero que muy, resolutivas.
Bueno, vale. ¿Y qué funciones son éstas? Pues enumerate(), filter(), map(), y reduce().
A estas funciones se las conoce como Funciones de Orden Superior que, como ya hemos dicho, proceden de la sintaxis propia de la programación Funcional, de naturaleza más matemática, y que nos faculta pasar distintas funciones como parámetros de otras devolviendo un resultado, habitualmente, en tipo de dato lista, el tipo de dato "preferido" por Python.
Sin haberlo citado de manera expresa, ya hemos visto algunas de estas Funciones de Orden Superior cuando estudiamos las funciones lambda o la compresión/descompresión de listas y diccionarios. Por el hecho de trabajar con listas, esto es, iterables, se las conoce también como Técnicas de Iteración de Orden Superior, centrándose en la recursividad (toda iteración, por su propia naturaleza, es a la vez recursión) y en el anidamiento a distintos niveles (listas anidadas, funciones anidadas, etc, que ya hemos visto en este manual).
Veamos a modo de introducción un ejemplo propuesto por Patricia Borensztein, profesora del Departamento de Computación de la Facultad de Ciencias Exactas y Naturales de la Universidad de Buenos Aires.


Por cierto, contamos con una segunda opción desde el punto de vista sintáctico para ejecutar una función: el doble paréntesis, ( ), que en algunos lenguajes, como en javascript, suponen una herramienta de autoejecución, inmediatamente adjunto a la llamada a la función que se ejecuta de manera directa, es decir, sin asignarla a variable alguna. Veámoslo.


Así que, sin más dilaciones, que el tiempo es oro, vamos a por ellas.

MAR DE NUBES DESDE LA CALDERA DE LA OROTAVA, CENTRO-NORTE DE TENERIFE.

       A) enumerate():

      

 
      Permite actuar sobre secuencias (iterables)  recurriendo a los índices (index) de los elementos constitutivos de cada una de las secuencias. Así, podemos escribir el siguiente sencillo código que nos permite comprobarlo, en este caso, con un una única lista:


A través de los índices podemos interrelacionar varias secuencias y entrelazar sus ítems respectivos entre sí (lo que obliga, claro está, a que todas las secuencias contengan número similar de elementos, o lo que es lo mismo, que el resultado de len() para todas ellas devuelva el mismo entero) lo que nos devolverá un resultado que ligará de forma coherente los ítems de sus listas de procedencia.
De este modo, al índice 0, esto es, lista[0], le corresponden todos aquellos valores que tengan el índice 0 cualquiera que fuera su lista de procedencia. Al índice 1, esto es, lista[1], le corresponden todos aquellos valores que tengan el índice 1 cualquiera que fuera su lista de procedencia. Y así sucesivamente.
Como para recorrer índices necesitamos hacer iteraciones correlativas, tenemos que recurrir a nuestro archifamoso iterador for/in que actuará sobre el índice (for indice), que quedarán asociados a los elementos (ítems) de la secuencia de partida:


Seguidamente, dentro del bucle for/in declaramos una variable que apuntará a sendos índices de cada secuencia, y cerramos instruyendo un formato de salida (str.format()).
Cualquier secuencia es potencialmente válida siempre y cuando, recordemos, sea un iterable (pues, de lo contrario, no podríamos acceder a sus índices) y contenga un número similar de ítems, lo que nos permitirá correlacionar datos de la manera esperada.
Veámoslo con varios ejemplos que introducen, esta vez, más de un iterable:



Ahora, rizamos el rizo: con tres iterables:



Entre una lista y una tupla:




LAGAR TRADICIONAL EN EL CASCO DE ARGUAYO, COMARCA DE ISORA, OESTE DE TENERIFE.

      B) filter():

     

      Como su nombre da a entender, esta función de orden superior aplica un filtro, que debemos pasar como primer argumento, obligatorio, a una función o, en su defecto, la sentencia None, y un iterable, normalmente, una lista, que pasamos como segundo argumento, obviamente, también obligatorio.
Desempeña, verdaderamente, un ejercicio de filtrado pues devuelve una lista (filter object, si nos ponemos cursis) que recorre todos aquellos elementos del iterable que le pasamos como argumento, y los incorpora a una lista, filter object, eso sí, siempre y cuando cumpla con los requisitos expresados en el bloque de código de la función que pasamos como primer argumento de filter() en base a un simple análisis booleano: si la ejecución de la función sobre un elemento del iterable devuelve True, éste se incorpora con bombo y platillo a filter object; si el resultado es False, sencillamente se descarta.



Observemos que cualquiera que fuere el iterable que le pasemos a filter() siempre nos devuelve una lista. Por el contrario, la inclusión de la sentencia None en lugar del nombre de una función, devuelve una copia del iterable como una lista.


Veamos un nuevo ejemplo a continuación en la que pasamos como argumento una función lambda y el correspondiente iterable:


Una función filter() se puede sustituir siempre bien por una función generator, o bien por una lista por compresión.


✱ Analizaremos más adelante, en una entrada, los defectos y virtudes de la función generator.

 
FLORACIÓN DEL BEJEQUE EN LA ZONA DE LOS CARRIZALES, MACIZO DE TENO, NOROESTE DE TENERIFE.

      C) map():

      


      Traduciendo la captura superior, la función map() muestra un espacio de argumentos más o menos similar al de la función filter() precedente, en tanto que pasamos una función como primer argumento obligatorio, que aplicaremos sobre los distintos ítems de los distintos iterables, *,  que pasemos como segundo argumento poniendo en relación los elementos de los mismos a través de sus índices respectivos (lo que, subsidiariamente nos obliga a pasar iterables con el mismo número de elementos, esto es, con el mismo len()) de acuerdo a los requisitos expresados en el cuerpo de la función que pasamos como primer argumento. He aquí la diferencia con filter(), donde sólo podíamos pasar un iterable por vez y, por consiguiente, no se establece relación alguna entre elementos de distintos iterables dado que sólo hay uno. Como consecuencia devuelve un objeto map, igual que filter() nos devolvía un objeto filter, que no es otra cosa, en ambos casos, que un iterable con los resultados obtenidos.


Vamos con nuestro primer ejemplo:



Como vemos, en 1. definimos una función  que llevará tantos argumentos obligatorios como iterables vayamos a pasar. En 2. establecemos en el cuerpo de la función exponent(b, e) la expresión que queremos que se ejecute: un algoritmo que devuelva la exponenciación sobre b de e.
En 3. y 4., declaramos los iterables que, en este caso, y por aquéllo de 'rizar el rizo', serán una lista y una tupla respectivamente, con lo que demostramos que no importa el tipo de dato que pasemos siempre y cuando sean iterables. Como ya hemos mencionado en la presentación de map(), ambos iterables deben contener el mismo número de elementos: len(iterable_lista) == len(iterable_tupla) de modo que, al ítem con el índice iterable_lista[0] del primer iterable se interrelacione con su homólogo iterable_tupla[0] del segundo iterable, y que al ítem con el índice iterable_lista[1] del primer iterable se interrelacione con su homólogo iterable_tupla[1] del segundo iterable, y así sucesivamente hasta haber recorrido por completo ambos iterables. Este procedimiento de asignación, comparable al que se da en los diccionarios con los pares clave/valor es lo que en programación se denomina "mapear", "hacer un mapeado".


      EN ALGUNOS LENGUAJES DE PROGRAMACIÓN A LOS DICCIONARIOS SE LOS LLAMA MAPS. Y EN LO QUE ATAÑE A PYTHON, ES POR ESTE MOTIVO QUE A NUESTRA FLAMANTE FUNCIÓN DE ORDEN SUPERIOR LA LLAMEMOS map() EN LUGAR DE PEPE LUIS O MARÍA CRISTINA. ANÁLOGAMENTE, EL DESEMPAQUETADO DE UNA LISTA CONSTITUYE, POR EJEMPLO, UNA SUERTE DE MAPEADO SUI GENERIS.





Para finalizar, en 5. invocamos a la función conversora list() (subrayemos, una vez más, que la devolución de map() es un objeto map que, a su vez, es de tipo lista) que llevará como argumento a la propia función map(), con el nombre de la función que hayamos definido como primer argumento y, a continuación, las correspondientes referencias a los iterables.
Debemos emplear la función list(), o la variable que le asignemos, si queremos ver el resultado impreso en pantalla pues, de lo contrario, bien sea con la función dedicada print() o bien a través de otra función de llamada a través de la sentencia return, sólo se nos mostrará su ubicación en memoria: no ocurrirá así si asignamos list() a una variable. Recordemos que la función print() imprime aquéllo que hayamos asignado previamente a una variable y no una devolución de manera directa.
Por supuesto, podemos mapear iterables con un único elemento, lo que en el caso del ejemplo que mostramos a continuación equivale a escribir h = pow(2, 7):


Si pasáramos a map() más de dos argumentos iterables, como hemos visto hasta ahora en los ejemplos, la función pasada como primer argumento se aplicará indistintamente sobre todos los indices coincidentes de cada iterable, fueran estos dos, tres, 50 o 100.


Donde pal1[0] se adjunta sobre pal2[0], y el resultado se adjunta a su vez sobre pal3[0]; pal1[1] se adjunta sobre pal2[1], y el resultado se adjunta a su vez sobre pal3[1] y. finalmente, pal1[2] se adjunta sobre pal2[2], y el resultado se adjunta a su vez sobre pal3[2].
En resumidas cuentas, tal y como mostramos en el esquema que sigue, lo deseable es que si se pasa a la función definida n cantidad de parámetros, la función map() incorpore igual número de iterables.


Puede ocurrir, a pesar de todo (y de nuestras reiteradas advertencias) que los iterables tengan un tamaño distinto. Esto es,que la longitud, len(iterable) de alguno de ellos no fuera coincidente con, al menos, otro de los iterables. En este caso, la función map() iterará tantas veces como elementos encuentre en el iterable de mayor tamaño. Python ejecutará el código mientras coincidan los índices de cada uno y de izquierda a derecha. Los que falten (o sobren), simplemente, se ignorarán.


Podemos incluir, como iterable que es, un diccionario, por ejemplo, pero tengamos en cuenta que, contrariamente a lo que pudiéramos pensar, la relación se establecerá con las claves (keys) pero no con los valores (values). ¡Ojo!👀


Terminamos con un ejemplo más de uso de la función map() para no pasar hambre:


CASERÍO DE LAS PALMAS DE ANAGA Y VISTA DE LOS ROQUES DE DENTRO Y FUERA, MACIZO DE ANAGA, NORESTE DE TENERIFE.

      D) reduce():


      La función reduce() ya no existe en Python 3 como función integrada, pero sí es posible importarla desde el módulo functools (funcfunctions, funciones; tools → herramientas), un módulo de la librería estándar de Python (stdlib) que nos proporciona varias funciones más de alto nivel como herramientas útiles para mejorar la claridad y eficiencia de nuestro código.
La función reduce() aplica una función a un iterable, ejecutándose de manera reiterada y de izquierda a derecha, sobre pares de elementos hasta mostrar un único valor. Actúa ejecutando la función con los dos primeros valores del iterable (iterable[0] e iterable[1]) y, a partir de aquí, el resultado con el tercero; el resultado de lo anterior con el cuarto; el resultado de lo anterior con el quinto; y así sucesivamente hasta conseguir un único valor.


120 es el resultado de multiplicar: 1 * 2 = 2, 2 * 3 = 6, 6 * 4 = 24 y 24 * 5 = 120.
Si se proporciona un valor opcional como argumento, dicho valor se instala automáticamente a la izquierda del iterable (asume la posición de iterable[0] desplazando en 1 a todos los demás elementos, los elementos originales) y, a partir de aquí, comienza a ejecutarse la función:


Podemos optar incluso por una función lambda la cual, en sí misma, es también una función de orden superior.


ANAGA Y SU NIEBLA ABRAZANDO LA MONTAÑA, MACIZO DE ANAGA, NORESTE DE TENERIFE.


No hay comentarios:

Publicar un comentario