Programación Funcional con Python
Esta notebook fue creada originalmente como un blog post por Raúl E. López Briega en Mi blog sobre Python. El contenido esta bajo la licencia BSD.
Introducción
Es bien sabido que existen muchas formas de resolver un mismo problema, esto, llevado al mundo de la programación, a generado que existan o co-existan diferentes estilos en los que podemos programar, los cuales son llamados generalmente paradigmas. Así, podemos encontrar basicamente 4 paradigmas principales de programación:
-
Programación imperativa: Este suele ser el primer paradigma con el que nos encontramos, el mismo describe a la programación en términos de un conjunto de intrucciones que modifican el estado del programa y especifican claramente cómo se deben realizar las cosas y modificar ese estado. Este paradigma esta representado por el lenguaje C.
-
Programación lógica: En este paradigma los programas son escritos en forma declarativa utilizando expresiones lógicas. El principal exponente es el lenguaje Prolog (programar en este esotérico lenguaje suele ser una experiencia interesante!).
-
Programación Orientada a Objetos: La idea básica detrás de este paradigma es que tanto los datos como las funciones que operan sobre estos datos deben estar contenidos en un mismo objeto. Estos objetos son entidades que tienen un determinado estado, comportamiento (método) e identidad. La Programación Orientada a Objetos es sumamente utilizada en el desarrollo de software actual; uno de sus principales impulsores es el lenguaje de programación Java.
-
Programación Funcional: Este último paradigma enfatiza la utilización de funciones puras, es decir, funciones que no tengan efectos secundarios, que no manejan datos mutables o de estado. Esta en clara contraposición con la programación imperativa. Uno de sus principales representantes es el lenguaje Haskell (lenguaje, que compite en belleza, elegancia y expresividad con Python!).
La mayoría de los lenguajes modernos son multiparadigma, es decir, nos permiten programar utilizando más de uno de los paradigmas arriba descritos. En este artículo voy a intentar explicar como podemos aplicar la Programación Funcional con Python.
¿Por qué Programación Funcional?
En estos últimos años hemos visto el resurgimiento de la Programación Funcional, nuevos lenguajes como Scala y Apple Swift ya traen por defecto montones de herramientas para facilitar el paradigma funcional. La principales razones del crecimiento de la popularidad de la Programación Funcional son:
- Los programas escritos en un estilo funcional son más fáciles de testear y depurar.
- Por su característica modular facilita la computación concurrente y paralela; permitiendonos obtener muchas más ventajas de los procesadores multinúcleo modernos.
- El estilo funcional se lleva muy bien con los datos; permitiendonos crear algoritmos y programas más expresivos para manejar la enorme cantidad de datos de la Big Data.(Aplicar el estilo funcional me suele recordar a utilizar las formulas en Excel).
Programación Funcional con Python
Antes de comenzar con ejemplos les voy a mencionar algunos de los modulos que que nos facilitan la Programación Funcional en Python, ellos son:
-
Intertools: Este es un modulo que viene ya instalado con la distribución oficial de Python; nos brinda un gran número de herramientas para facilitarnos la creación de iteradores.
-
Operator: Este modulo también la vamos a encontrar ya instalado con Python, en el vamos a poder encontrar a los principales operadores de Python convertidos en funciones.
-
Functools: También ya incluido dentro de Python este modulo nos ayuda a crear Funciones de orden superior, es decir, funciones que actuan sobre o nos devuelven otras funciones.
-
Fn: Este modulo, creado por Alexey Kachayev, brinda a Python las “baterías” adicionales para hacer el estilo funcional de programación mucho más fácil.
-
Cytoolz: Modulo creado por Erik Welch que también nos proporciona varias herramientas para la Programación Funcional, especialmente orientado a operaciones de análisis de datos.
-
Macropy: Este modulo, creado por Li Haoyi trae a Python características propias de los lenguajes puramente funcionales, como ser, pattern matching, tail call optimization, y case classes.
Ejemplos
Utilizando Map, Reduce, Filter y Zip
Cuando tenemos que realizar operaciones sobre listas, en lugar de utilizar los clásicos loops, podemos utilizar las funciones Map, Reduce, Filter y Zip.
Map
La función Map nos permite aplicar una operación sobre cada uno de los items de una lista. El primer argumento es la función que vamos a aplicar y el segundo argumento es la lista.
#creamos una lista de números del 1 al 10
items = list(xrange(1, 11))
items
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
#creamos una lista de los cuadrados de la lista items.
#forma imperativa.
cuadrados = []
for i in items:
cuadrados.append(i ** 2)
cuadrados
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
#Cuadrados utilizando Map.
#forma funcional
cuadrados = map(lambda x: x **2, items)
cuadrados
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Como podemos ver, al utilizar map las líneas de código se reducen y nuestro programa es mucho más simple de comprender. En el ejemplo le estamos pasando a map una función anónima o lambda. Esta es otra característica que nos ofrece Python para la Programación Funcional. Map también puede ser utilizado con funciones de más de un argumento y más de una lista, por ejemplo:
#importamos pow.
from math import pow
#como vemos la función pow toma dos argumentos, un número y su potencia.
pow(2, 3)
8.0
#si tenemos las siguientes listas
numeros = [2, 3, 4]
potencias = [3, 2, 4]
#podemos aplicar map con pow y las dos listas.
#nos devolvera una sola lista con las potencias aplicadas sobre los números.
potenciados = map(pow, numeros, potencias)
potenciados
[8.0, 9.0, 256.0]
Reduce
La función Reduce reduce los valores de la lista a un solo valor aplicando una funcion reductora. El primer argumento es la función reductora que vamos a aplicar y el segundo argumento es la lista.
#Sumando los valores de la lista items.
#forma imperativa
suma = 0
for i in items:
suma += i
suma
55
#Suma utilizando Reduce.
#Forma funcional
from functools import reduce #en python3 reduce se encuentra en modulo functools
suma = reduce(lambda x, y: x + y, items)
suma
55
La función Reduce también cuenta con un tercer argumento que es el valor inicial o default. Por ejemplo si quisiéramos sumarle 10 a la suma de los elementos de la lista items, solo tendríamos que agregar el tercer argumento.
#10 + suma items
suma10 = reduce(lambda x, y: x + y, items, 10)
suma10
65
Filter
La función Filter nos ofrece una forma muy elegante de filtrar elementos de una lista.El primer argumento es la función filtradora que vamos a aplicar y el segundo argumento es la lista.
#Numeros pares de la lista items.
#Forma imperativa.
pares = []
for i in items:
if i % 2 ==0:
pares.append(i)
pares
[2, 4, 6, 8, 10]
#Pares utilizando Filter
#Forma funcional.
pares = filter(lambda x: x % 2 == 0, items)
pares
[2, 4, 6, 8, 10]
Zip
Zip es una función para reorganizar listas. Como parámetros admite un conjunto de listas. Lo hace es tomar el elemento iésimo de cada lista y unirlos en una tupla, después une todas las tuplas en una sola lista.
#Ejemplo de zip
nombres = ["Raul", "Pedro", "Sofia"]
apellidos = ["Lopez Briega", "Perez", "Gonzalez"]
#zip une cada nombre con su apellido en una lista de tuplas.
nombreApellido = zip(nombres, apellidos)
nombreApellido
[('Raul', 'Lopez Briega'), ('Pedro', 'Perez'), ('Sofia', 'Gonzalez')]
Removiendo Efectos Secundarios
Una de las buenas practicas que hace al estilo funcional es siempre tratar de evitar los efectos secundarios, es decir, evitar que nuestras funciones modifiquen los valores de sus parámetros, así en lugar de escribir código como el siguiente:
#Funcion que no sigue las buenas practias de la programacion funcional.
#Esta funcion tiene efectos secundarios, ya que modifica la lista que se le pasa como argumento.
def cuadrados(lista):
for i, v in enumerate(lista):
lista[i] = v ** 2
return lista
Deberíamos escribir código como el siguiente, el cual evita los efectos secundarios:
#Version funcional de la funcion anterior.
def fcuadrados(lista):
return map(lambda x: x ** 2, lista)
#Aplicando fcuadrados sobre items.
fcuadrados(items)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
#items no se modifico
items
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
#aplicando cuadrados sobre items
cuadrados(items)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
#Esta función tiene efecto secundario.
#items fue modificado por cuadrados.
items
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Al escribir funciones que no tengan efectos secundarios nos vamos a ahorrar muchos dolores de cabeza ocasionados por la modificación involuntaria de objetos.
Utilizando el modulo Fn.py
Algunas de las cosas que nos ofrece este modulo son: Estructuras de datos inmutables, lambdas al estilo de Scala, lazy evaluation de streams, nuevas Funciones de orden superior, entre otras.
#Lambdas al estilo scala
from fn import _
(_ + _)(10, 3)
13
items = list(xrange(1,11))
cuadrados = map( _ ** 2, items)
cuadrados
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
#Streams
from fn import Stream
s = Stream() << [1,2,3,4,5]
s
<fn.stream.Stream at 0x7f873c1d7a70>
list(s)
[1, 2, 3, 4, 5]
s[1]
2
s << [6, 7, 8, 9]
<fn.stream.Stream at 0x7f873c1d7a70>
s[6]
7
#Stream fibonacci
from fn.iters import take, drop, map as imap
from operator import add
f = Stream()
fib = f << [0, 1] << imap(add, f, drop(1, f))
#primeros 10 elementos de fibonacci
list(take(10, fib))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
#elemento 20 de la secuencia fibonacci
fib[20]
6765
#elementos 40 al 45 de la secuencia fibonacci
list(fib[40:45])
[102334155, 165580141, 267914296, 433494437, 701408733]
#Funciones de orden superior
from fn import F
from operator import add, mul #operadores de suma y multiplicacion
#composición de funciones
F(add, 1)(10)
11
#f es una funcion que llama a otra funcion.
f = F(add, 5) << F(mul, 100) #<< operador de composicion de funciones.
#cada valor de la lista primero se multiplica por 100 y luego
#se le suma 5, segun composicion de f de arriba.
map(f, [0, 1, 2, 3])
[5, 105, 205, 305]
func = F() >> (filter, _ < 6) >> sum
#func primero filtra los valores menores a 6
#y luego los suma.
func(xrange(10))
15
Utilizando el modulo cytoolz
Este modulo nos provee varias herramienta para trabajar con funciones, iteradores y diccionarios.
#Datos a utilizar en los ejemplos
cuentas = [(1, 'Alice', 100, 'F'), # id, nombre, balance, sexo
(2, 'Bob', 200, 'M'),
(3, 'Charlie', 150, 'M'),
(4, 'Dennis', 50, 'M'),
(5, 'Edith', 300, 'F')]
from cytoolz.curried import pipe, map as cmap, filter as cfilter, get
#seleccionando el id y el nombre de los que tienen un balance mayor a 150
pipe(cuentas, cfilter(lambda (id, nombre, balance, sexo): balance > 150),
cmap(get([1, 2])),
list)
[('Bob', 200), ('Edith', 300)]
#este mismo resultado tambien lo podemos lograr con las listas por comprensión.
#mas pythonico.
[(nombre, balance) for (id, nombre, balance, sexo) in cuentas
if balance > 150]
[('Bob', 200), ('Edith', 300)]
from cytoolz import groupby
#agrupando por sexo
groupby(get(3), cuentas)
{'F': [(1, 'Alice', 100, 'F'), (5, 'Edith', 300, 'F')],
'M': [(2, 'Bob', 200, 'M'), (3, 'Charlie', 150, 'M'), (4, 'Dennis', 50, 'M')]}
#utilizando reduceby
from cytoolz import reduceby
def iseven(n):
return n % 2 == 0
def add(x, y):
return x + y
reduceby(iseven, add, [1, 2, 3, 4])
{False: 4, True: 6}
Ordenando objectos con operator itemgetter, attrgetter y methodcaller
Existen tres funciones dignas de mención en el modulo operator, las cuales nos permiten ordenar todo tipo de objetos en forma muy sencilla, ellas son itemgetter, attrgetter y methodcaller.
#Datos para los ejemplos
estudiantes_tupla = [
('john', 'A', 15),
('jane', 'B', 12),
('dave', 'B', 10),
]
class Estudiante:
def __init__(self, nombre, nota, edad):
self.nombre = nombre
self.nota = nota
self.edad = edad
def __repr__(self):
return repr((self.nombre, self.nota, self.edad))
def nota_ponderada(self):
return 'CBA'.index(self.nota) / float(self.edad)
estudiantes_objeto = [
Estudiante('john', 'A', 15),
Estudiante('jane', 'B', 12),
Estudiante('dave', 'B', 10),
]
from operator import itemgetter, attrgetter, methodcaller
#ordenar por edad tupla
sorted(estudiantes_tupla, key=itemgetter(2))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
#ordenar por edad objetos
sorted(estudiantes_objeto, key=attrgetter('edad'))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
#ordenar por nota y edad tupla
sorted(estudiantes_tupla, key=itemgetter(1,2))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
#ordenar por nota y edad objetos
sorted(estudiantes_objeto, key=attrgetter('nota', 'edad'))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
#ordenando por el resultado del metodo nota_ponderada
sorted(estudiantes_objeto, key=methodcaller('nota_ponderada'))
[('jane', 'B', 12), ('dave', 'B', 10), ('john', 'A', 15)]
Hasta aquí llega esta introducción. Tengan en cuenta que Python no es un lenguaje puramente funcional, por lo que algunas soluciones pueden verse más como un hack y no ser del todo pythonicas. El concepto más importante es el de evitar los efectos secundarios en nuestras funciones. Debemos mantener un equilibrio entre los diferentes paradigmas y utilizar las opciones que nos ofrece Python que haga más legible nuestro código. Para más información sobre la Programación Funcional en Python también puede visitar el siguiente documento y darse una vuelta por la documentación de los módulos mencionados más arriba. Por último, los que quieran incursionar con un lenguaje puramente funcional, les recomiendo Haskell.
Saludos!
Este post fue escrito utilizando IPython notebook. Pueden descargar este notebook o ver su version estática en nbviewer.