Programación funcional en Python

El popular lenguaje de programación Python es más bien conocido por la programación orientada a objetos, pero en realidad Python también es apto para la programación funcional. Te explicamos qué funciones ofrece y cómo se utilizan.

¿En qué consiste la programación funcional?

El concepto “programación funcional” hace referencia a un estilo de programación que utiliza las funciones como unidad básica de código. El espectro es gradual, hay desde lenguajes puramente funcionales como Haskell o Lisp, hasta lenguajes multiparadigma como Python, así que no es tan fácil separar los lenguajes que soportan la programación funcional.

Para que un lenguaje permita la programación funcional, debe tratar a las funciones como ciudadanos de primera clase. Es lo que ocurre con Python; las funciones son objetos, al igual que los strings, los números y las listas. Las funciones pueden pasarse como parámetros a otras funciones o devolverse como valores de retorno de funciones.

La programación funcional es declarativa

Con la programación declarativa, se describe un problema y es el entorno del lenguaje el que se encarga de solucionarlo. En cambio, con un enfoque imperativo se describe paso a paso todo el proceso de solución. La programación funcional forma parte del entorno declarativo y Python permite utilizar ambos enfoques.

A continuación, verás un ejemplo concreto de Python. Imagina que tienes una lista de números y quieres calcular los cuadrados correspondientes. Primero, con un enfoque imperativo:

# Calculate squares from list of numbers
def squared(nums):
    # Start with empty list
    squares = []
    # Process each number individually
    for num in nums:
        squares.append(num ** 2)
    return squares
Python

Python aplica un enfoque declarativo a la list comprehension, que puede combinarse adecuadamente con técnicas funcionales. Creamos la lista de números cuadrado sin ciclos explícitos. El código que se origina es mucho más elegante y no necesita sangrías:

# Numbers 0–9
nums = range(10)
# Calculate squares using list expression
squares = [num ** 2 for num in nums]
# Show that both methods give the same result
assert squares == squared(nums)
Python

Preferencia de las funciones puras a los procedimientos

Una función pura es comparable a una función matemática. Este concepto describe una función con las siguientes propiedades:

• La función proporciona el mismo resultado al mismo argumento

• La función tiene acceso exclusivo a sus argumentos

• La función no tiene efectos secundarios

En conjunto, todas estas propiedades indican que, al ejecutar una función pura, el sistema que lo rodea no cambia. A continuación, hay un ejemplo clásico de la función cuadrada f(x) = x x*, que en Python puede aplicarse fácilmente como función pura.

def f(x):
    return x * x
# let's test
assert f(9) == 81
Python

En contraposición a las funciones puras están los procedimientos comunes en lenguajes más antiguos como Pascal y Basic. Al igual que una función, un procedimiento es un bloque de código al que puede llamarse muchas veces. Sin embargo, el procedimiento no devuelve otro valor, sino que accede directamente a las variables no locales y las modifica según sea necesario.

En C y Java, los procedimientos se hacen como función con tipo de retorno void. En Python, una función siempre devuelve un valor. Si no hay una declaración de retorno, devolverá el valor especial “None”. Por tanto, en lo que respecta a los procedimientos en Python, se trata de una función sin declaración de retorno.

A continuación, verás varios ejemplos de funciones puras e impuras en Python. La siguiente función es impura dado que en cada llamada da un resultado distinto:

# Function without arguments
def get_date():
    from datetime import datetime
    return datetime.now()
Python

El siguiente procedimiento es impuro ya que accede a datos definidos fuera de la función:

# Function using non-local value
name = 'John'
def greetings_from_outside():
    return(f"Greetings from {name}")
Python

La siguiente función es impura porque modifica un argumento mutable en la llamada y así influye en el sistema en el que se encuentra:

# Function modifying argument
def greetings_from(person):
    print(f"Greetings from {person['name']}")
    # Changing `person` defined somewhere else
    person['greeted'] = True
    return person
# Let's test
person = {'name': "John"}
# Prints `John`
greetings_from(person)
# Data was changed from inside function
assert person['greeted']
Python

Esta función es pura porque devuelve el mismo resultado para el mismo argumento y no tiene efectos secundarios:

# Pure function
def squared(num):
    return num * num
Python

La recursión como alternativa a la iteración

En la programación funcional, la recursión es lo contrario a la iteración. Una función recursiva se llama a sí misma repetidamente para conseguir un resultado. Para que esto suceda sin que la función genere un bucle infinito, deben cumplirse dos condiciones:

  1. La recursión debe finalizar al alcanzar un caso base
  2. Al recorrer recursivamente la función, debe reducirse el problema

Python soporta las funciones recursivas. Ahora verás el famoso ejemplo del cálculo de la secuencia de Fibonacci. Se conoce como el enfoque ingenuo. No sirve para grandes valores de n, pero se optimiza bien con el almacenamiento en caché.

def fib(n):
    if n == 0 or n == 1:
        return n
    else:
        return fib(n - 2) + fib(n - 1)
Python

¿Cómo de compatible es Python con la programación funcional?

Python es un lenguaje multiparadigma, por lo que puede seguir múltiples paradigmas de programación. Además de la programación funcional, Python puede recurrir a la programación orientada a objetos sin problema alguno.

Python cuenta con un amplio abanico de herramientas para programación funcional, pero a diferencia de lo que ocurre con lenguajes funcionales puros (como Haskell) su entorno está muy limitado. El nivel de programación funcional de Python depende principalmente del programador. He a continuación una lista de las características funcionales más importantes de Python.

Las funciones en Python son ciudadanos de primera clase

La máxima de Python de “Everything is an object” (Todo son objetos) también vale para las funciones. Las funciones pueden introducirse en cualquier lugar en el que se permita meter otros objetos. Un ejemplo concreto: imagina que quieres programar una calculadora de bolsillo que permita hacer múltiples operaciones.

Primero tienes el enfoque imperativo, que usa las herramientas clásicas de la programación estructurada como las ramas condicionales y las secuencias de asignación:

def calculate(a, b, op='+'):
    if op == '+':
        result = a + b
    elif op == '-':
        result = a - b
    elif op == '*':
        result = a * b
    elif op == '/':
        result = a / b
    return result
Python

He a continuación un enfoque declarativo para resolver el mismo problema. En vez de la condición if, se construyen las operaciones como dict de Python. Aquí, los signos matemáticos se usan como claves para los objetos de función correspondientes que importamos del operador módulo. Este código es más claro y no requiere ramificaciones:

def calculate(a, b, op='+'):
    # Import operator functions
    import operator
    # Map operation symbols to functions
    operations = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
    }
    # Choose operation to carry out
    operation = operations[op]
    # Run operation and return results
    return operation(a, b)
Python

Luego, entra en juego la función declarativa calculate. La instrucción assert demuestra que el código funciona:

# Let's test
a, b = 42, 51
assert calculate(a, b, '+') == a + b
assert calculate(a, b, '-') == a - b
assert calculate(a, b, '*') == a* b
assert calculate(a, b, '/') == a / b
Python

Las lambdas son funciones anónimas en Python

Además de la manera ya conocida de definir las funciones en Python con la palabra clave def, está la opción de las “lambdas”. Son funciones cortas y anónimas (es decir, sin nombre) que definen una expresión con parámetros. Las lambdas pueden usarse en cualquier lugar en el que se espere una función o pueda vincularse a un nombre con una asignación:

squared = lambda x: x * x
assert squared(9) == 81
Python

Las lambdas permiten mejorar la función calculate, ya que en vez de codificar las operaciones disponibles dentro de la función, entregas un dict con funciones lambda como valor, lo cual permite añadir operaciones nuevas con más facilidad:

def calculate(a, b, op, ops={}):
    # Get operation from dict and define noop for non-existing key
    operation = ops.get(op, lambda a, b: None)
    return operation(a, b)
# Define operations
operations = {
    '+': lambda a, b: a + b,
    '-': lambda a, b: a - b,
}
# Let's test
a, b, = 42, 51
assert calculate(a, b, '+', operations) == a + b
assert calculate(a, b, '-', operations) == a - b
# Non-existing key handled gracefully
assert calculate(a, b, '**', operations) == None
# Add a new operation
operations['**'] = lambda a, b: a** b
assert calculate(a, b, '**', operations) == a** b
Python

Funciones de orden superior en Python

Las lambdas suelen combinarse con funciones de orden superior como map() y filter(). Con ellas pueden transformarse los elementos de un iterable sin usar bucles. La función map() toma como parámetros una función y un iterable y ejecuta la función con cada elemento del iterable. En este ejemplo se trata de nuevo el problema de los números cuadrados:

nums = [3, 5, 7]
squares = map(lambda x: x ** 2, nums)
assert list(squares) == [9, 25, 49]
Python
Nota

Las funciones de orden superior (en inglés higher order functions) son funciones que aceptan funciones como parámetros o devuelven una función.

Con la función filter() pueden filtrarse los elementos de un iterable. Así se amplía el ejemplo para solo crear números cuadrados:

nums = [1, 2, 3, 4]
squares = list(map(lambda num: num ** 2, nums))
even_squares = filter(lambda square: square % 2 == 0, squares)
assert list(even_squares) == [4, 16]
Python

Iterables, comprehensions y generadores

Los iterables son un concepto básico de Python. Se trata de una abstracción sobre las colecciones cuyos elementos pueden emitirse individualmente. Entre ellos están los strings, las tuplas, las listas y los dicts, todos siguiendo las mismas reglas. Puedes consultar el ámbito de un iterable con la función len():

name = 'Walter White'
assert len(name) == 12
people = ['Jim', 'Jack', 'John']
assert len(people) == 3
Python

A partir de los iterables pueden usarse las comprehensions. Son idóneas para la programación funcional y han sustituido considerablemente las lambdas con map() y filter().

# List comprehension to create first ten squares
squares = [num ** 2 for num in range(10)]
Python

Como ya se conoce de los lenguajes puramente funcionales, con los generadores, Python dispone de un enfoque para la evaluación perezosa. Esto significa que los datos solo se generan cuando se accede a ellos, lo que permite ahorrar mucha memoria. Abajo tienes una expresión de generador que al acceder calcula cada número cuadrado:

# Generator expression to create first ten squares
squares = (num ** 2 for num in range(10))
Python

Las funciones perezosas pueden ejecutarse en Python con una asignación yield. Aquí tienes una función que da números positivos hasta un límite establecido:

def N(limit):
    n = 1
    while n <= limit:
        yield n
        n += 1
Python

¿Qué alternativas a Python hay para la programación funcional?

Hace tiempo que la programación funcional goza de gran popularidad y que se ha establecido como el principal competidor de la programación orientada a objetos. La combinación de estructuras de datos inmutables y funciones puras crea un código fácilmente paralelizable, por lo que la programación funcional es especialmente interesante para transformar datos en canalización de datos.

Los lenguajes puramente funcionales con sistemas de tipos fuertes, como Haskell o el dialecto de Lisp Clojure, son particularmente potentes. Por otra parte, JavaScript, en el fondo, también se considera un lenguaje funcional. TypeScript es una alternativa moderna con un tipado fuerte.

Consejo

¿Quieres trabajar con Python online? Quizá te convenga alquilar hosting con espacio ilimitado para tu proyecto.