Programación Funcional en Python
En pocas palabras, la programación funcional es un paradigma de programación distinto al tradicional estructurado u orientado a objetos al que solemos estar acostumbrados. Se basa principalmente en el uso de funciones, siendo las mismas practicamente la única herramienta que el lenguaje nos ofrece. Por ello, en lenguajes puramente funcionales como Haskell no existen bucles for o while.
¿Un lenguaje de programación sin bucles? Aunque pueda parecer una locura, también tiene sus ventajas, y ofrece ciertas características muy importantes que veremos a continuación.
A pesar de que Python no es un lenguaje puramente funcional, nos ofrece algunas primitivas propias de lenguajes funcionales, como map
, filter
y reduce
. Todas ellas ofrecen una alternativa al uso de bucles para resolver ciertos problemas. Veamos unos ejemplos.
map en Python
La función map
toma dos entradas:
- Una lista o iterable que será modificado en una nueva.
- Una función, que será aplicada a cada uno de los elementos de la lista o iterable anterior.
Nos devuelve una nueva lista donde todos y cada uno de los elementos de la lista original han sido pasados por la función.
map(funcion_a_aplicar, entrada_iterable)
Imaginemos que queremos multiplicar por dos todos los elementos de una lista. La primera forma que se nos podría ocurrir sería la siguiente. Nótese que también podría usarse list comprehension, pero eso lo dejamos para otro artículo.
lista = [1, 2, 3, 4, 5]
lista_pordos = []
for l in lista:
lista_pordos.append(l*2)
print(lista_pordos)
# [2, 4, 6, 8, 10]
Pero veamos como darle un enfoque funcional. Como hemos dicho, map
toma una función y un iterable como entrada, aplicando la función a cada elemento. Entonces podemos definir una función por_dos
, que pasaremos a map
junto con nuestra lista inicial.
lista = [1, 2, 3, 4, 5]
def por_dos(x):
return x * 2
lista_pordos = map(por_dos, lista)
print(list(lista_pordos))
# [2, 4, 6, 8, 10]
Como podemos observar, ya no usamos bucles. Simplemente le pasamos a map
la función y la lista y ya hace el trabajo por nosotros. Dado que por_dos
se trata de una función muy sencilla, es posible usar funciones lambda para compactar un poco el código, quedando lo siguiente:
lista = [1, 2, 3, 4, 5]
lista_pordos = map(lambda x: 2*x, lista)
print(list(lista_pordos))
# [2, 4, 6, 8, 10]
filter en Python
La función filter
también recibe una función y una lista pero el resultado es la lista inicial filtrada. Es decir, se pasa cada elemento de la lista por la función, y sólo si su resultado es True
, se incluye en la nueva lista.
filter(funcion_filtrar, entrada_iterable)
Al igual que hacíamos antes, usamos las funciones lambda que nos permiten declarar y asignar una función en la misma línea de código. En el siguiente ejemplo filtramos los números pares.
lista = [7, 4, 16, 3, 8]
pares = filter(lambda x: x % 2 == 0, lista)
print(list(pares))
# [4, 16, 8]
Nótese que el siguiente código sería equivalente:
lista = [7, 4, 16, 3, 8]
def es_par(x):
return x % 2 == 0
pares = filter(es_par, lista)
print(list(pares))
# [4, 16, 8]
Una vez más, resaltar que no estamos usando bucles. Simplemente damos la entrada y la función a aplicar a cada elemento, y filter
se encarga del resto. Esta es una de las características clave de la programación funcional. Se centra más en el qué hacer que en el cómo hacerlo. Para ello se usan funciones predefinidas como las que estamos viendo, a las que sólo tenemos que pasar las entradas y hacer el trabajo por nosotros.
reduce en Python
Por último, podemos usar reduce
para reducir todos los elementos de la entrada a un único valor aplicando un determinado criterio. Por ejemplo, podemos sumar todos los elementos de una lista de la siguiente manera.
from functools import reduce
lista = [1, 2, 3, 4, 5]
suma = reduce(lambda acc, val: acc + val, lista)
print(suma) # 15
Lo que podría reescribirse usando la función add
:
from operator import add
from functools import reduce
lista = [1, 2, 3, 4, 5]
suma = reduce(add, lista)
print(suma) # 15
O también los podemos multiplicar, modificando la función lambda.
from functools import reduce
lista = [1, 2, 3, 4, 5]
multiplicacion = reduce(lambda acc, val: acc * val, lista)
print(multiplicacion) # 120
Es importante notar que la función recibe dos argumentos: el acumulador y cada uno de los valores de la lista.
- El acumulador es el valor devuelto en la iteración anterior, que va acumulando un resultado hasta que llegamos al final.
- El valor es cada uno de los elementos de nuestra lista, que en nuestro caso vamos añadiendo al acumulador.
El uso de reduce
es especialmente útil cuando tenemos por ejemplo una lista de diccionarios y queremos sumar todos los valores de un campo en concreto. Veamos un ejemplo donde calculamos la edad media de varias personas.
from functools import reduce
personas = [
{'Nombre': 'Alicia', 'Edad': 22},
{'Nombre': 'Bob', 'Edad': 29},
{'Nombre': 'Charlie', 'Edad': 33}
]
suma_edad = reduce(lambda total, p: total + p['Edad'], personas, 0)
print(suma_edad/len(personas)) # 28.0
Nótese que llamamos a reduce
con un valor adicional 0
, que es el valor inicial del acumulador. Una vez más, hemos resuelto un problema en el que tradicionalmente usaríamos bucles con las primitivas de la programación funcional.
Características de la Programación Funcional
Una vez entendido el funcionamiento de map
, filter
y reduce
, ya tenemos unas nociones básicas de la programación funcional. Sus características más importantes son las siguientes:
- Los lenguajes de programación puramente funcionales carecen de bucles for y while.
- Se dice que en la programación funcional, las funciones son “ciudadanas de primera clase”, lo que nos viene a decir que prácticamente todo se hace con funciones, y no con bucles.
- La programación funcional prefiere también las funciones puras, es decir, funciones sin side effects. Las funciones puras no dependen de variables externas o globales. Esto significa que para las mismas entradas, siempre se producen las mismas salidas.
- Por otro lado, en la programación funcional se prefiere variables inmutables, lo que significa que una vez creadas no pueden ser modificadas. Esto es algo que verdaderamente ahorra problemas, aunque la eficiencia puede ser discutible.
- En general, se considera que los lenguajes de programación funcionales son más seguros, y ofrecen formal verification.
Por último, resaltar una vez más que aunque Python no es un lenguaje puramente funcional, ofrece algunas funciones propias de lenguajes funcionales como map
, filter
y reduce
. Si estás interesado en más, puedes echar un vistazo a el paquete itertools.
Ejemplos Programación Funcional
Dada una lista de personas almacenadas en diccionarios:
- Filtrar de acuerdo al sexo
- Y calcular la media
from functools import reduce
personas = [
{'Nombre': 'Alicia', 'Edad': 22, 'Sexo': 'F'},
{'Nombre': 'Bob', 'Edad': 25, 'Sexo': 'M'},
{'Nombre': 'Charlie', 'Edad': 33, 'Sexo': 'M'},
{'Nombre': 'Diana', 'Edad': 15, 'Sexo': 'F'},
{'Nombre': 'Esteban', 'Edad': 30, 'Sexo': 'M'},
{'Nombre': 'Federico', 'Edad': 44, 'Sexo': 'M'},
]
hombres = list(filter(lambda x: x['Sexo'] == 'M', personas))
suma_edades = reduce(lambda suma, p: suma + p['Edad'], hombres, 0)
media_edad = suma_edades/(len(hombres))
print(media_edad) # 33.0
Tal vez no muy legible, pero todo lo anterior se podrá expresar en una única línea de código.
media_edades = reduce(lambda suma, p: suma + p['Edad'], filter(lambda x: x['Sexo'] == 'M', personas), 0) / len(list(filter(lambda x: x['Sexo'] == 'M', personas)))
print(media_edades) # 33.0