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:

  1. Los programas escritos en un estilo funcional son más fáciles de testear y depurar.
  2. Por su característica modular facilita la computación concurrente y paralela; permitiendonos obtener muchas más ventajas de los procesadores multinúcleo modernos.
  3. 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.
  • 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.

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.


In [1]:
#creamos una lista de números del 1 al 10
items = list(xrange(1, 11))
items


Out[1]:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [2]:
#creamos una lista de los cuadrados de la lista items.
#forma imperativa.
cuadrados = []
for i in items:
    cuadrados.append(i ** 2)
    
cuadrados


Out[2]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

In [3]:
#Cuadrados utilizando Map.
#forma funcional
cuadrados = map(lambda x: x **2, items)
cuadrados


Out[3]:
[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:


In [4]:
#importamos pow.
from math import pow

In [5]:
#como vemos la función pow toma dos argumentos, un número y su potencia.
pow(2, 3)


Out[5]:
8.0

In [6]:
#si tenemos las siguientes listas 
numeros = [2, 3, 4]
potencias = [3, 2, 4]

In [7]:
#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


Out[7]:
[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.


In [8]:
#Sumando los valores de la lista items.
#forma imperativa
suma = 0
for i in items:
    suma += i

suma


Out[8]:
55

In [9]:
#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


Out[9]:
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.


In [10]:
#10 + suma items
suma10 = reduce(lambda x, y: x + y, items, 10)
suma10


Out[10]:
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.


In [11]:
#Numeros pares de la lista items.
#Forma imperativa.
pares = []
for i in items:
    if i % 2 ==0:
        pares.append(i)
        
pares


Out[11]:
[2, 4, 6, 8, 10]

In [12]:
#Pares utilizando Filter
#Forma funcional.
pares = filter(lambda x: x % 2 == 0, items)
pares


Out[12]:
[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.


In [13]:
#Ejemplo de zip
nombres = ["Raul", "Pedro", "Sofia"]
apellidos = ["Lopez Briega", "Perez", "Gonzalez"]

In [14]:
#zip une cada nombre con su apellido en una lista de tuplas.
nombreApellido = zip(nombres, apellidos)
nombreApellido


Out[14]:
[('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:


In [15]:
#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:


In [16]:
#Version funcional de la funcion anterior.
def fcuadrados(lista):
    return map(lambda x: x ** 2, lista)

In [17]:
#Aplicando fcuadrados sobre items.
fcuadrados(items)


Out[17]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

In [18]:
#items no se modifico
items


Out[18]:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [19]:
#aplicando cuadrados sobre items
cuadrados(items)


Out[19]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

In [20]:
#Esta función tiene efecto secundario.
#items fue modificado por cuadrados.
items


Out[20]:
[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.


In [21]:
#Lambdas al estilo scala
from fn import _

(_ + _)(10, 3)


Out[21]:
13

In [22]:
items = list(xrange(1,11))

In [23]:
cuadrados = map( _ ** 2, items)
cuadrados


Out[23]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

In [24]:
#Streams
from fn import Stream

s = Stream() << [1,2,3,4,5]
s


Out[24]:
<fn.stream.Stream at 0x7f873c1d7a70>

In [25]:
list(s)


Out[25]:
[1, 2, 3, 4, 5]

In [26]:
s[1]


Out[26]:
2

In [27]:
s << [6, 7, 8, 9]


Out[27]:
<fn.stream.Stream at 0x7f873c1d7a70>

In [28]:
s[6]


Out[28]:
7

In [29]:
#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))


Out[29]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

In [30]:
#elemento 20 de la secuencia fibonacci
fib[20]


Out[30]:
6765

In [31]:
#elementos 40 al 45 de la secuencia fibonacci
list(fib[40:45])


Out[31]:
[102334155, 165580141, 267914296, 433494437, 701408733]

In [32]:
#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)


Out[32]:
11

In [33]:
#f es una funcion que llama a otra funcion.
f = F(add, 5) << F(mul, 100) #<< operador de composicion de funciones.

In [34]:
#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])


Out[34]:
[5, 105, 205, 305]

In [35]:
func = F() >> (filter, _ < 6) >> sum

In [36]:
#func primero filtra los valores menores a 6
#y luego los suma.
func(xrange(10))


Out[36]:
15

Utilizando el modulo cytoolz

Este modulo nos provee varias herramienta para trabajar con funciones, iteradores y diccionarios.


In [37]:
#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')]

In [38]:
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)


Out[38]:
[('Bob', 200), ('Edith', 300)]

In [39]:
#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]


Out[39]:
[('Bob', 200), ('Edith', 300)]

In [40]:
from cytoolz import groupby

#agrupando por sexo 
groupby(get(3), cuentas)


Out[40]:
{'F': [(1, 'Alice', 100, 'F'), (5, 'Edith', 300, 'F')],
 'M': [(2, 'Bob', 200, 'M'), (3, 'Charlie', 150, 'M'), (4, 'Dennis', 50, 'M')]}

In [41]:
#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])


Out[41]:
{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.


In [42]:
#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),
]

In [43]:
from operator import itemgetter, attrgetter, methodcaller

#ordenar por edad tupla
sorted(estudiantes_tupla, key=itemgetter(2))


Out[43]:
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

In [44]:
#ordenar por edad objetos
sorted(estudiantes_objeto, key=attrgetter('edad'))


Out[44]:
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

In [45]:
#ordenar por nota y edad tupla
sorted(estudiantes_tupla, key=itemgetter(1,2))


Out[45]:
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]

In [46]:
#ordenar por nota y edad objetos
sorted(estudiantes_objeto, key=attrgetter('nota', 'edad'))


Out[46]:
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]

In [47]:
#ordenando por el resultado del metodo nota_ponderada
sorted(estudiantes_objeto, key=methodcaller('nota_ponderada'))


Out[47]:
[('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.