El popular lenguaje de pro­gra­ma­ción Python es más bien conocido por la pro­gra­ma­ción orientada a objetos, pero en realidad Python también es apto para la pro­gra­ma­ción funcional. Te ex­pli­ca­mos qué funciones ofrece y cómo se utilizan.

¿En qué consiste la pro­gra­ma­ción funcional?

El concepto “pro­gra­ma­ción funcional” hace re­fe­re­n­cia a un estilo de pro­gra­ma­ción que utiliza las funciones como unidad básica de código. El espectro es gradual, hay desde lenguajes puramente fu­n­cio­na­les como Haskell o Lisp, hasta lenguajes mu­l­ti­pa­ra­di­g­ma como Python, así que no es tan fácil separar los lenguajes que soportan la pro­gra­ma­ción funcional.

Para que un lenguaje permita la pro­gra­ma­ción funcional, debe tratar a las funciones como ciu­da­da­nos 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 pa­rá­me­tros a otras funciones o de­vo­l­ve­r­se como valores de retorno de funciones.

La pro­gra­ma­ción funcional es de­cla­ra­ti­va

Con la pro­gra­ma­ción de­cla­ra­ti­va, se describe un problema y es el entorno del lenguaje el que se encarga de so­lu­cio­nar­lo. En cambio, con un enfoque im­pe­ra­ti­vo se describe paso a paso todo el proceso de solución. La pro­gra­ma­ción funcional forma parte del entorno de­cla­ra­ti­vo y Python permite utilizar ambos enfoques.

A co­n­ti­nua­ción, verás un ejemplo concreto de Python. Imagina que tienes una lista de números y quieres calcular los cuadrados co­rre­s­po­n­die­n­tes. Primero, con un enfoque im­pe­ra­ti­vo:

# 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 de­cla­ra­ti­vo a la list co­m­prehe­n­sion, que puede co­m­bi­nar­se ade­cua­da­me­n­te con técnicas fu­n­cio­na­les. Creamos la lista de números cuadrado sin ciclos ex­plí­ci­tos. 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

Pre­fe­re­n­cia de las funciones puras a los pro­ce­di­mie­n­tos

Una función pura es co­m­pa­ra­ble a una función ma­te­má­ti­ca. Este concepto describe una función con las si­guie­n­tes pro­pie­da­des:

• La función pro­po­r­cio­na el mismo resultado al mismo argumento

• La función tiene acceso exclusivo a sus ar­gu­me­n­tos

• La función no tiene efectos se­cu­n­da­rios

En conjunto, todas estas pro­pie­da­des indican que, al ejecutar una función pura, el sistema que lo rodea no cambia. A co­n­ti­nua­ción, hay un ejemplo clásico de la función cuadrada f(x) = x x*, que en Python puede aplicarse fá­ci­l­me­n­te como función pura.

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

En co­n­tra­po­si­ción a las funciones puras están los pro­ce­di­mie­n­tos comunes en lenguajes más antiguos como Pascal y Basic. Al igual que una función, un pro­ce­di­mie­n­to es un bloque de código al que puede llamarse muchas veces. Sin embargo, el pro­ce­di­mie­n­to no devuelve otro valor, sino que accede di­re­c­ta­me­n­te a las variables no locales y las modifica según sea necesario.

En C y Java, los pro­ce­di­mie­n­tos se hacen como función con tipo de retorno void. En Python, una función siempre devuelve un valor. Si no hay una de­cla­ra­ción de retorno, devolverá el valor especial “None”. Por tanto, en lo que respecta a los pro­ce­di­mie­n­tos en Python, se trata de una función sin de­cla­ra­ción de retorno.

A co­n­ti­nua­ció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 pro­ce­di­mie­n­to 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 se­cu­n­da­rios:

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

La recursión como al­te­r­na­ti­va a la iteración

En la pro­gra­ma­ción funcional, la recursión es lo contrario a la iteración. Una función recursiva se llama a sí misma re­pe­ti­da­me­n­te para conseguir un resultado. Para que esto suceda sin que la función genere un bucle infinito, deben cumplirse dos co­n­di­cio­nes:

  1. La recursión debe finalizar al alcanzar un caso base
  2. Al recorrer re­cu­r­si­va­me­n­te la función, debe reducirse el problema

Python soporta las funciones re­cu­r­si­vas. 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 al­ma­ce­na­mie­n­to 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 co­m­pa­ti­ble es Python con la pro­gra­ma­ción funcional?

Python es un lenguaje mu­l­ti­pa­ra­di­g­ma, por lo que puede seguir múltiples pa­ra­di­g­mas de pro­gra­ma­ción. Además de la pro­gra­ma­ción funcional, Python puede recurrir a la pro­gra­ma­ción orientada a objetos sin problema alguno.

Python cuenta con un amplio abanico de he­rra­mie­n­tas para pro­gra­ma­ción funcional, pero a di­fe­re­n­cia de lo que ocurre con lenguajes fu­n­cio­na­les puros (como Haskell) su entorno está muy limitado. El nivel de pro­gra­ma­ción funcional de Python depende pri­n­ci­pa­l­me­n­te del pro­gra­ma­dor. He a co­n­ti­nua­ción una lista de las ca­ra­c­te­rí­s­ti­cas fu­n­cio­na­les más im­po­r­ta­n­tes de Python.

Las funciones en Python son ciu­da­da­nos de primera clase

La máxima de Python de “Eve­r­y­thi­ng is an object” (Todo son objetos) también vale para las funciones. Las funciones pueden in­tro­du­ci­r­se en cualquier lugar en el que se permita meter otros objetos. Un ejemplo concreto: imagina que quieres programar una ca­l­cu­la­do­ra de bolsillo que permita hacer múltiples ope­ra­cio­nes.

Primero tienes el enfoque im­pe­ra­ti­vo, que usa las he­rra­mie­n­tas clásicas de la pro­gra­ma­ción es­tru­c­tu­ra­da como las ramas co­n­di­cio­na­les y las se­cue­n­cias de asi­g­na­ció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 co­n­ti­nua­ción un enfoque de­cla­ra­ti­vo para resolver el mismo problema. En vez de la condición if, se co­n­s­tru­yen las ope­ra­cio­nes como dict de Python. Aquí, los signos ma­te­má­ti­cos se usan como claves para los objetos de función co­rre­s­po­n­die­n­tes que im­po­r­ta­mos del operador módulo. Este código es más claro y no requiere ra­mi­fi­ca­cio­nes:

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 de­cla­ra­ti­va calculate. La in­s­tru­c­ció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 pa­rá­me­tros. Las lambdas pueden usarse en cualquier lugar en el que se espere una función o pueda vi­n­cu­lar­se a un nombre con una asi­g­na­ció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 ope­ra­cio­nes di­s­po­ni­bles dentro de la función, entregas un dict con funciones lambda como valor, lo cual permite añadir ope­ra­cio­nes 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 co­m­bi­nar­se con funciones de orden superior como map() y filter(). Con ellas pueden tra­n­s­fo­r­mar­se los elementos de un iterable sin usar bucles. La función map() toma como pa­rá­me­tros 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 pa­rá­me­tros 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, co­m­prehe­n­sio­ns y ge­ne­ra­do­res

Los iterables son un concepto básico de Python. Se trata de una ab­s­tra­c­ción sobre las co­le­c­cio­nes cuyos elementos pueden emitirse in­di­vi­dua­l­me­n­te. 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 co­m­prehe­n­sio­ns. Son idóneas para la pro­gra­ma­ción funcional y han su­s­ti­tui­do co­n­si­de­ra­ble­me­n­te 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 fu­n­cio­na­les, con los ge­ne­ra­do­res, Python dispone de un enfoque para la eva­lua­ció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 eje­cu­tar­se en Python con una asi­g­na­ción yield. Aquí tienes una función que da números positivos hasta un límite es­ta­ble­ci­do:

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

¿Qué al­te­r­na­ti­vas a Python hay para la pro­gra­ma­ción funcional?

Hace tiempo que la pro­gra­ma­ción funcional goza de gran po­pu­la­ri­dad y que se ha es­ta­ble­ci­do como el principal co­m­pe­ti­dor de la pro­gra­ma­ción orientada a objetos. La co­m­bi­na­ción de es­tru­c­tu­ras de datos in­mu­ta­bles y funciones puras crea un código fá­ci­l­me­n­te pa­ra­le­li­za­ble, por lo que la pro­gra­ma­ción funcional es es­pe­cia­l­me­n­te in­te­re­sa­n­te para tra­n­s­fo­r­mar datos en ca­na­li­za­ción de datos.

Los lenguajes puramente fu­n­cio­na­les con sistemas de tipos fuertes, como Haskell o el dialecto de Lisp Clojure, son pa­r­ti­cu­la­r­me­n­te potentes. Por otra parte, Ja­va­S­cri­pt, en el fondo, también se considera un lenguaje funcional. Ty­pe­S­cri­pt es una al­te­r­na­ti­va moderna con un tipado fuerte.

Consejo

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

Ir al menú principal