Introducción a la programación funcional en Python | Programador Web Valencia

Introducción a la programación funcional en Python

8 minutos

Python

Desde hace unos años se ha vuelto a poner de moda trabajar con programación funcional, como si las nuevas generaciones hubieran re-descubierto las ventajas frente a la programación orientada a objetos o empezaran a tomar consciencia de las limitaciones en su forma de trabajar. Por ello mismo me parece interesante que resolvamos algunas dudas como ¿qué es? ¿para que sirve? ¿por donde puedo empezar? Y si es con Python, mucho mejor.

¿Qué significa que Python es multiparadigma?

Según Wikipedia:

“Python es un lenguaje de programación interpretado cuya filosofía hace hincapié en la legibilidad de su código. Se trata de un lenguaje de programación multiparadigma, ya que soporta orientación a objetos, programación imperativa y, en menor medida, programación funcional.”

Quiere decir que acepta diversas formar de trabajar con el lenguaje. Básicamente existen 3 paradigmas predominantes, o dicho de otra manera, 3 formatos globales de organizar un código.

  • Programación imperativa (estructurada), el código será ejecutado desde el principio del fichero al final sin seguir ningún tipo de desviación. Su mayor ventaja radica en su simplicidad y poco peso. Su peligrosidad es el código espagueti, archivos con centenares o miles de líneas donde solo unos pocos seres humanos son capaces de modificar y salir victoriosos.

  • Programación orientada a objetos (OOP o Object Oriented Programming), donde se encapsulan las variables y funciones en pequeños módulos capaces de clonarse y modificarse. Su punto fuerte es la capacidad de re-utilización y aislamiento para evitar problemas con otras funcionalidades. La parte negativa recae en la complejidad de crear buenos objetos y la depuración.

  • Programación funcional (FP o Functional programming), donde el código se reparte en sencillas funciones capaces de ser invocadas con variables u otras funciones. Su facilidad de uso por atomicidad logra un mantenimiento sólido y compatible con casi cualquier lenguaje. Además su inmutabilidad de variables evita gran parte de los problemas que si sufre la programación orientada a objetos.

Si buscar profundizar en sus diferencias disponéis del libro Arquitectura limpia de Robert C. Martin (tio Sam).

Principios de la programación funcional

  • Uso de funciones: Como su nombre indica, todo se construye por medio de funciones. Esta forma de trabajar no solo es sencilla, ordenada, clara, fácil de testear, sino que además es una práctica que han utilizado grandes figuras militares como Julio César y Napoleón. No, no usaron Python (al menos no hay constancia de ello), pero aplicaron el concepto de: “divide y vencerás”. Y la programación funcional usa esta estrategia para prácticamente todo.
  • Funciones de primera clase: Las funciones son tratadas como una variable más. Incluso pueden ser devueltas.
  • Funciones puras: Totalmente predictivo, los mismos datos de entrada producirán los mismos datos de salida. Puedes sustituir el parámetro de entrada de sin que ello altere el flujo del programa.
  • Recursividad: Las funciones se pueden llamar a si mismas simplificando tareas como recorrer árboles de datos o la gestión de bucles controlados.
  • Inmutabilidad: No hay variables, solo constantes. Personalmente comprender su potencial y llevarlo a la práctica fue como darle al reset de mi cerebro; tuve que re-aprender a usar variable. Anécdota a parte; ¿donde suele fallar el software? En gran mayoría de las ocasiones viene por una variable que a sido cambiada. Esto provoca que un bloque de código se ejecute con unas condiciones no previstas por nadie en el mundo mundial, toca revisar cada variable en diferentes valores hasta que encontramos al culpable. Os hago una reflexión: ¿Y si esas variables nunca fueran modificadas? O siendo más prácticos, ¿y si creamos una nueva constante de cada modificación? ¿Y si… os digo que a nivel de rendimiento… es más eficiente? Es un concepto muy interesante de aplicar.
  • Evaluación perezosa (no estricta): En la programación funcional podemos trabajar con expresiones que no han sido evaluadas, o dicho de otra manera, podemos disponer de variables con operaciones cuyo resultado aún no se conoce. A esto se le denomina evaluación no estricta. Un efecto secundario es el aumento de rendimiento, y otra es que podemos realizar locuras como hacer cálculos con operaciones muy complejas o listas infinitas sin realizar calculos. ¿Cómo es esto posible? Porque se trabaja con expresiones matemáticas, solo se calcula el valor cuando lo necesitas como por ejemplo al realizar un print.

¿Qué te aporta como programador?

Te estarás diciendo en estos momentos: “Ya se programación orientada a objetos, ¿que gano aprendiendo técnicas funcionales?”.

  • Simplificar la creación de concurrencias. Una de sus grandes ventajas y por la cual se hizo tan popular con Lisp para crear redes neuronales. Podemos trabajar simultáneamente con diversas funciones sin efectos secundarios.
  • Disminución de problemas. Las variables son constantes, son inmutables. En consecuencia, no hay errores de programación derivados del llamado “estado global mutables” (estado modificable por todo el código), como puede ser el caso de los proyectos orientados a objetos.
  • Rápido de testear, sabemos que parámetros podemos dar a una función y que resultados esperamos.
  • Fácil de combinar con la programación imperativa y orientada a objetos. En este caso Python adopta herramientas nativas desde la versión 3.0 .
  • Código dócil de asimilar y leer. Es más cómodo entender una función que la estructura de un objeto. Por supuesto hablo desde el punto de vista de una persona que acaba de llegar a un proyecto o esta aprendiendo a programar.

Ejemplos

Quiero adelantar que solo voy a enseñar unos ejemplos básicos para iniciarse, la punta de un Iceberg. Para los curiosos quedan todo un océano de soluciones y herramientas propias de lenguajes funcionales que Python no conoce o debe ser extendido con librerías. Vamos a tratar funciones de orden superior: lambda, closures, filter, map y reduce.

Iceberg

lambda

Los lenguajes funcionales tienen su origen en la lógica matemática y el cálculo lambda, mientras que los lenguajes de programación imperativos abrazan el modelo de cálculo basado en el estado (inventado por Alan Turing).

¿Qué son los Lambdas en Python? En pocas palabras: pequeñas funciones anónimas, restrictivas y concisas.

Así haríamos en una función tradicional.

def incrementar(x):
    return x + 1

Podéis observar que su objetivo es incrementar un número. Y de este modo se construiría en un lambda.

lambda x: x + 1

Es únicamente una expresión, no la puedes utilizar; pero hay 2 formas de sacarle partido.

Con paréntesis.

(lambda x: x + 1)(9)
# 10

O guardando en una variable.

incremento = lambda x: x + 1
incremento(24)
# 25

closures

Recordar el concepto de Funciones de primera clase. Podemos usar funciones dentro de otras funciones y además funciones que devuelvan otras.

# Función que devuelve una función
def construir_multiplos(factor):
    def interno(valor):
        return valor * factor
    return interno

# Guardamos la expresión
multiplos_de_2 = construir_multiplos(2)
multiplos_de_7 = construir_multiplos(7)

# Evaluamos

multiplos_de_2(10)
# 20

multiplos_de_7(2)
# 14

filter()

Para los siguientes ejemplos usaré un super diccionario con 4 elementos.

superheroes = [
    {
        "nombre": "Batman",
        "editorial": "DC Comics",
        "alter_ego": "Bruce Wayne",
        "primera_aparicion": "Detective Comics #27"
    },
    {
        "nombre": "Superman",
        "editorial": "DC Comics",
        "alter_ego": "Kal-El",
        "primera_aparicion": "Action Comics #1"
    },
    {
        "nombre": "Spider Man",
        "editorial": "Marvel Comics",
        "alter_ego": "Peter Parker",
        "primera_aparicion": "Amazing Fantasy #15"
    },
    {
        "nombre": "Hulk",
        "editorial": "Marvel Comics",
        "alter_ego": "Bruce Banner",
        "primera_aparicion": "The Incredible Hulk #1"
    }
]

Empecemos con filter. Su objetivo es convertir un elemento iterable (como una tupla o lista), en otra pero de igual o inferior tamaño; filtrando por lo que quieras.

Su estructura costa de 2 argumentos: la expresión y el elemento que deseamos recorrer.

filter(funcion, elemento_iterable)

Busquemos todos los superheroes de DC.

superheroesDC = filter(lambda superheroe: superheroe['editorial'] == "DC Comics", superheroes)

print(tuple(superheroesDC))

Como resultado tendremos.

({
  'nombre': 'Batman',
  'editorial': 'DC Comics',
  'alter_ego': 'Bruce Wayne',
  'primera_aparicion':
  'Detective Comics #27'
},{
  'nombre': 'Superman',
  'editorial': 'DC Comics',
  'alter_ego': 'Kal-El',
  'primera_aparicion': 'Action Comics #1'
})

map()

Su estructura costa de 2 argumentos: la expresión y el elemento que deseamos recorrer. En este caso se utiliza para modificar el valor de una secuencia siguiendo una regla.

map(funcion, elemento_iterable)

En el siguiente ejemplo voy a añadir a cada superheroe su poder. Como no quiero entrar en guerras innecesarias le daré un valor aleatorio entre 0 y 99.

No es recomendable usar un lambda si vas a usar más de una línea en la función.

import random

def anyadirPoder(superheroe):
    superheroe.update(poder=random.randint(0, 100))
    return superheroe

superheroesPoder = map(anyadirPoder, superheroes)

print(tuple(superheroesPoder))

El resultado sería el siguiente.

({
  'nombre': 'Batman',
  'editorial': 'DC Comics',
  'alter_ego': 'Bruce Wayne',
  'primera_aparicion': 'Detective Comics #27',
  'poder': 88
}, {
  'nombre': 'Superman',
  'editorial': 'DC Comics',
  'alter_ego': 'Kal-El',
  'primera_aparicion': 'Action Comics #1',
  'poder': 49
}, {
  'nombre': 'Spider Man',
  'editorial': 'Marvel Comics',
  'alter_ego': 'Peter Parker',
  'primera_aparicion': 'Amazing Fantasy #15',
  'poder': 35
}, {
  'nombre': 'Hulk',
  'editorial': 'Marvel Comics',
  'alter_ego': 'Bruce Banner',
  'primera_aparicion': 'The Incredible Hulk #1',
  'poder': 58
})

reduce()

Se utiliza para hacer cálculos a partir de una iteración. Su estructura costa de 3 argumentos: la expresión, el elemento que deseamos recorrer y la variable de inicio. La podemos encuentra dentro de la librería functools.

import functools

functools.reduce(funcion, elemento_iterable, variable_inicial)

Supongamos que necesitamos contar el números de superheroe que contiene el texto “man” en su nombre.

import functools

def contarMan(resultado, superheroe):
    if 'man' in superheroe['nombre'].lower()j:
        return resultado + 1
    else:
        return resultado

total = functools.reduce(contarMan, superheroes, 0)

print(total)
# 3

Te preguntarás, ¿por qué esta escondida? Estas funciones las podemos encontrar desde principios de 1994, pero durante el desarrollo de Python 3000 (entre 2006 y 2009), Guido van Rossum (creador de Python) pidió la eliminación de estas características por un tema de legibilidad y por existir herramientas que en conjunto realizaban las mismas tareas. Sin embargo, más tarde cambió de opinión, y solo el reduce fue eliminado a pesar de que sigue siendo accesible a través de los módulos de la librería estándar functools.

Transparencia referencial

Una de las técnicas más comunes, dentro de la programación funcional, es evitar efectos colaterales con la transparencia referencial. Significa que al usar una función nunca se debe modificar ninguna variable, de ningún lugar del código. A cambio nos devolverá siempre un valor. Python es bastante propenso a este tipo de daños, pero podemos esquivalos con algunas estrategias.

Por ejemplo, cuando quiera añadir un nuevo valor a un diccionario puedo copiarlo y devolverlo con el valor nuevo.

PERFIL = {"id": 1, "name": "Spiderman"}

print({**PERFIL, **{"email": "spider@correo.com"}})

# {'id': 1, 'name': 'Spiderman', 'email': 'spider@correo.com'}

print(PERFIL)

# {"id": 1, "name": "Spiderman"}

No se más modificado PERFIL, pero en cambio hemos sido capaces de agregar una nueva clave.

Un buen uso sería dentro del ejempo de anyadirPoder.

import random

def anyadirPoder(superheroe):
    return {**superheroe, **{"poder": random.randint(0, 100)}}

superheroesPoder = map(anyadirPoder, superheroes)

print(tuple(superheroesPoder))

Ahora sin duda queda más limpio y seguro.

Lenguajes funcionales

No solo de Python vive el hombre. Para exprimir las posibilidades que hemos mencionado puedes dar el salto a otros lenguajes propios del paradigma.

  • Lisp, dispone de diversas variantes pero la más popular es Common Lisp que lleva dando guerra desde 1984.
  • Clojure, una modernización de Lisp que ha vuelto a revitalizar el lenguaje. Utiliza la máquina virtual de Java para ejecutarse y dispone de una interoperabilidad fantástica con las librerías de Java. Ideal para realizar servicios web o software con gran rendimiento. Aprovecho para comentar que dispongo de un Framework Web llamado Tadam idea para iniciarse.
  • Haskell, pionero en diversas características. Maravilloso para el aprendizaje y muy utilizado en la industria.
  • Elm, lenguaje para Front-End altamente influenciado por Haskell.

Compañías que usan algun lenguaje funcional

La lista es grande, por lo que he realizado un filter con algunas compañías que usan Lisp o Clojure.

  • Apple.
  • Atlassian.
  • Boeing.
  • Netflix.
  • Nasa (como PVS).
  • iRobot (Roomba).
  • YCombinantor (Hacker News)
  • Nubank.
  • CircleCI.
  • Walmart.
  • Soundcloud.
  • RavenPack.

Extendiendo Python

Dentro de Python puedes usar la librería PyFunctional, la cual te aporta temas tan importantes como la concurrencia u operaciones concadenadas. También puedes usar otra librería como Frozendict para implementar diccionarios inmutables.

Espero que la programación funcional deje de asustarte porque no es el lado oscuro, sino una herramienta increíblemente potente que te está esperando.

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

¿Me ayudas?

Comprame un café
Pulsa sobre la imagen

No te sientas obligado a realizar una donación, pero cada aportación mantiene el sitio en activo logrando que continúe existiendo y sea accesible para otras personas. Además me motiva a crear nuevo contenido.

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario

Tal vez también te interese...