🥥=🐍+🐒
Iterables
Si duda los Iterables es la característica de programación funcional que más se usa en python. Se emplean en las compresiones de listas y las expresiones generadoras.
Pero veamos algunas definiciones:
- Iterable: objeto del que se puede recorrer sus elementos en orden, uno a uno. Como ejemplos, están los tipos
list
,str
,tuple
ydict
. Lo común es recorrer sus elementos con un buclefor
. - Iterador: objeto que representa un flujo de datos. Con cada uso, o bien entrega un dato, o bien produce una excepción por quedarse vacío. Por diseño, un
Iterador
deriva deIterable
. - Secuencia: iterable con acceso a la posición de cualquiera de sus elementos y con un tamaño conocido. Como ejemplos están los tipos
list
,str
ytuple
. El tipodict
también funciona como secuencia, pero no se considera como tal al no accederse a sus elementos por posición numérica. (Es más considerado como mapping). - Generador: objeto que crea iteradores. Hay funciones generadoras, que crean un iterador cada vez que se llaman, y expresiones generadoras, que crean un sólo iterador.
Iteradores y secuencias forman el conjunto de los iterables, y podemos identificarlos estructuralmente de la siguiente manera:
- Un Iterable suele tener un método
__iter__
que devuelve un iterador para recorrer en orden todos los elementos. - Un Iterador tiene un método
__next__
para entregar el siguiente dato. - Una Secuencia tiene un método
__getitem__
, para acceso a cualquier elemento, y un método__len__
, para conocer su tamaño. Además, pueden tener otros métodos comocount()
,index()
,__contains__()
y__reversed__()
con los que completar la clase base abstracta collections.abc.Sequence.
Importante
Aunque un Iterable se suele caracterizar por tener un método __iter__
, también las secuencias se consideran iterables aunque no tengan este método.
Caso práctico: analicemos un objeto range
(eg: dir(range(10))
):
- tiene el métodos
__iter__
—> es unIterable
- tiene los métodos
__getitem__
y__len__
—> es unaSecuencia
- tiene el resto de métodos de
collections.abc.Sequence
- no tiene método
__next__
Los objeto Range
son secuencias, no iteradores. Como secuencia, algunas de la operaciones que permite son:
- troceo:
range(100)[4:20] == range(4,20)
- obtener el tamaño:
len(range(100)) == 100
- chequeos de pertenencia:
200 in range(100) == False
- inversión:
range(100)[::-1] == range(99, -1, -1)
Composición de Iteradores
Una ventaja de usar iteradores en lugar de listas es que sólo necesitan memoria para procesar el elemento que están trabajado, por lo que los hace muy eficientes para procesar grandes cantidades de datos o ficheros enormes que no caben enteros en memoria.
Por ejemplo, esta expresión generadora:
(x**2 for x in range(10**100))
Como curiosidad, el número 10**100
se llama gúgol y es un número enormemente grande. Crear esto mismo como una lista es imposible con la memoria de los ordenadores actuales y no habría tiempo en este Universo para procesar tal cantidad de elementos.
Y sin embargo, podemos trabajar con esta expresión sin mayores problemas. Por ejemplo, para obtener los 10 primeros elementos:
g = (x**2 for x in range(10**100))
for i in range(10):
print(next(g))
Incluso podemos usarla como base para crear nuevos iteradores:
(y+1 for y in (x**2 for x in range(10**100)))
Equivalente a hacer:
(x**2+1 for x in range(10**100))
Componer iteradores es bastante eficiente. Se van recorriendo a medida que sea necesario, sin mantener en memoria nada más que los elementos estrictamente necesarios para devolver el siguiente elemento.
map y filter
¿Sabes cuál es la diferencia entre estas dos expresiones?
(x**2 for x in range(10*100))
map(lambda x: x**2, range(10*100))
Para python, ambas expresiones son equivalentes: iteradores. Pero se considera que tiene más estilo pythónico el uso de expresiones generadoras, desaconsejándose completamene el uso de las funciones map
y filter
siempre que se pueda.
Y sin embargo, para coconut las funciones map
y filter
son fundamentales como también lo son en programación funcional.
El iterador que se obtiene con la expresión generadora (x**2 for x in range(10*100))
actúa como si fuera una caja negra. Se pueden obtener los elemntos uno a uno, pero no ofrece información sobre su estructura interna, ni de su tamaño, ni nada que permita su transfomación.
En cambio, el iterador map en coconut (extensión de la función map
de python) almacena información tanto de la función como de los iterables a los que se aplica, cosa que hace en los atributos func
y iters
, respectivamente. De este modo, coconut puede encadenar transformaciones de manera más óptima.
Por ejemplo, para obtener el último elemento del iterador anterior:
map(x -> x**2, range(10*100))[-1]
Como iterador, debería haber pasado por todos los 10**100
elementos hasta llegar al último, cosa que es imposible de hacer en la práctica. Sin embargo, si se prueba en coconut, se obtiene el último elemento casi al instante. En realidad, coconut cortocircuita todo el proceso y salta directamente al último elemento de range(10**100)
, que es el único que le hace falta. Es como si hubiera aplicado primero el [-1]
al range(10**100)
y luego hubiera aplicado el map
.
Este tipo de encapsulado, tanto de un conjunto de datos como de la función que lo transforma, se conocen por aplicativo (Applicative en inglés). Y a las transformaciones que podemos encadenar, una tras otra, para diferir el cálculo al final del todo se conoce por mónadas (Monad en inglés).
Más adelante veremos una definición matemática formal, pero podemos considerar monad como la herramienta matemática más poderosa de un programador funcional y, algunas veces, también la más incomprensible.