Programación funcional en Python

La Programación Funcional es un paradigma de programación popular estrechamente relacionado con los fundamentos matemáticos de la informática. Si bien no existe una definición estricta de...

Introducción

La Programación Funcional es un paradigma de programación popular estrechamente relacionado con los fundamentos matemáticos de la informática. Si bien no existe una definición estricta de lo que constituye un lenguaje funcional, los consideramos lenguajes que usan funciones para transformar datos.

Python no es un lenguaje de programación funcional, pero incorpora algunos de sus conceptos junto con otros paradigmas de programación. Con Python, es fácil escribir código en un estilo funcional, lo que puede proporcionar la mejor solución para la tarea en cuestión.

Conceptos de Programación Funcional

Los lenguajes funcionales son lenguajes declarativos, le dicen a la computadora qué resultado quieren. Esto generalmente se contrasta con los lenguajes imperativos que le dicen a la computadora qué pasos tomar para resolver un problema. Python generalmente se codifica de manera imperativa, pero puede usar el estilo declarativo si es necesario.

Algunas de las características de Python fueron influenciadas por Haskell, un lenguaje de programación puramente funcional. Para obtener una mejor apreciación de lo que es un lenguaje funcional, echemos un vistazo a las características de Haskell que pueden verse como rasgos funcionales deseables:

  • Funciones puras: no tienen efectos secundarios, es decir, no cambian el estado del programa. Dada la misma entrada, una función pura siempre producirá la misma salida.
  • Inmutabilidad: los datos no se pueden cambiar después de crearlos. Tomemos, por ejemplo, la creación de una ‘Lista’ con 3 elementos y almacenarla en una variable ‘mi_lista’. Si my_list es inmutable, no podrá cambiar los elementos individuales. Tendría que establecer my_list en una nueva Lista si desea utilizar valores diferentes.
  • Funciones de orden superior: las funciones pueden aceptar otras funciones como parámetros y las funciones pueden devolver nuevas funciones como salida. Esto nos permite abstraernos de las acciones, dándonos flexibilidad en el comportamiento de nuestro código.

Haskell también ha influido en iteradores y generadores en Python a través de su carga diferida, pero esa característica no es necesaria para un lenguaje funcional.

Programación funcional en Python

Sin características o bibliotecas especiales de Python, podemos comenzar a codificar de una manera más funcional.

Funciones puras

Si desea que las funciones sean puras, no cambie el valor de la entrada ni ningún dato que exista fuera del alcance de la función.

Esto hace que la función que escribimos sea mucho más fácil de probar. Como no cambia el estado de ninguna variable, tenemos la garantía de obtener el mismo resultado cada vez que ejecutamos la función con la misma entrada.

Vamos a crear una función pura para multiplicar números por 2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def multiply_2_pure(numbers):
    new_numbers = []
    for n in numbers:
        new_numbers.append(n * 2)
    return new_numbers

original_numbers = [1, 3, 5, 10]
changed_numbers = multiply_2_pure(original_numbers)
print(original_numbers) # [1, 3, 5, 10]
print(changed_numbers)  # [2, 6, 10, 20]

La lista original de números no ha cambiado, y no hacemos referencia a ninguna otra variable fuera de la función, por lo que es pura.

Inmutabilidad

¿Alguna vez tuvo un error en el que se preguntó cómo una variable que estableció en 25 se convirtió en “Ninguna”? Si esa variable fuera inmutable, el error se habría producido donde se estaba cambiando la variable, no donde el valor modificado ya afectaba al software; la causa raíz del error se puede encontrar antes.

Python ofrece algunos tipos de datos inmutables, uno de los más populares es Tuple. Vamos a contrastar la tupla con una lista, que es mutable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
mutable_collection = ['Tim', 10, [4, 5]]
immutable_collection = ('Tim', 10, [4, 5])

# Reading from data types are essentially the same:
print(mutable_collection[2])    # [4, 5]
print(immutable_collection[2])  # [4, 5]

# Let's change the 2nd value from 10 to 15
mutable_collection[1] = 15

# This fails with the tuple
immutable_collection[1] = 15

El error que vería es: TypeError: el objeto 'tuple' no admite la asignación de elementos.

Ahora, hay un escenario interesante donde una ‘Tupla’ puede parecer un objeto mutable. Por ejemplo, si quisiéramos cambiar la lista en colección_inmutable de [4, 5] a [4, 5, 6], puedes hacer lo siguiente:

1
2
immutable_collection[2].append(6)
print(immutable_collection[2])  # [4, 5, 6]

Esto funciona porque una Lista es un objeto mutable. Intentemos volver a cambiar la lista a [4, 5].

1
2
3
immutable_collection[2] = [4, 5]
# This throws a familiar error:
# TypeError: 'tuple' object does not support item assignment

Falla tal como esperábamos. Si bien podemos cambiar el contenido de un objeto mutable en una Tupla, no podemos cambiar la referencia al objeto mutable que está almacenado en la memoria.

Funciones de orden superior {#funciones de orden superior}

Recuerde que las funciones de orden superior aceptan una función como argumento o devuelven una función para su posterior procesamiento. Ilustremos lo simple que se pueden crear ambos en Python.

Considere una función que imprime una línea varias veces:

1
2
3
4
5
def write_repeat(message, n):
    for i in range(n):
        print(message)

write_repeat('Hello', 5)

¿Qué pasaría si quisiéramos escribir en un archivo 5 veces o registrar el mensaje 5 veces? En lugar de escribir 3 funciones diferentes que se repiten, podemos escribir 1 función de orden superior que acepte esas funciones como argumento:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def hof_write_repeat(message, n, action):
    for i in range(n):
        action(message)

hof_write_repeat('Hello', 5, print)

# Import the logging library
import logging
# Log the output as an error instead
hof_write_repeat('Hello', 5, logging.error)

Ahora imagine que tenemos la tarea de crear funciones que incrementen los números en una lista en 2, 5 y 10. Comencemos con el primer caso:

1
2
3
4
5
6
7
def add2(numbers):
    new_numbers = []
    for n in numbers:
        new_numbers.append(n + 2)
    return new_numbers

print(add2([23, 88])) # [25, 90]

Si bien es trivial escribir las funciones add5 y add10, es obvio que operarían de la misma manera: recorriendo la lista y agregando el incrementador. Entonces, en lugar de crear muchas funciones de incremento diferentes, creamos 1 función de orden superior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def hof_add(increment):
    # Create a function that loops and adds the increment
    def add_increment(numbers):
        new_numbers = []
        for n in numbers:
            new_numbers.append(n + increment)
        return new_numbers
    # We return the function as we do any other value
    return add_increment

add5 = hof_add(5)
print(add5([23, 88]))   # [28, 93]
add10 = hof_add(10)
print(add10([23, 88]))  # [33, 98]

Las funciones de orden superior dan flexibilidad a nuestro código. Al abstraer qué funciones se aplican o devuelven, ganamos más control sobre el comportamiento de nuestro programa.

Python proporciona algunas funciones integradas de orden superior útiles, lo que hace que trabajar con secuencias sea mucho más fácil. Primero veremos las expresiones lambda para utilizar mejor estas funciones integradas.

Expresiones lambda {#expresiones lambda}

Una expresión lambda es una función anónima. Cuando creamos funciones en Python, usamos la palabra clave def y le damos un nombre. Las expresiones lambda nos permiten definir una función mucho más rápido.

Vamos a crear una función de orden superior hof_product que devuelve una función que multiplica un número por un valor predefinido:

1
2
3
4
5
def hof_product(multiplier):
    return lambda x: x * multiplier

mult6 = hof_product(6)
print(mult6(6)) # 36

La expresión lambda comienza con la palabra clave lambda seguida de los argumentos de la función. Después de los dos puntos está el código devuelto por la lambda. Esta capacidad de crear funciones "sobre la marcha" se usa mucho cuando se trabaja con funciones de orden superior.

Hay mucho más sobre las expresiones lambda que cubrimos en nuestro artículo Funciones Lambda en Python si quieres más información.

Funciones integradas de orden superior {#funciones integradas de orden superior}

Python ha implementado algunas funciones de orden superior comúnmente utilizadas de lenguajes de programación funcional que facilitan mucho el procesamiento de objetos iterables como listas e iteradores. Por razones de eficiencia de espacio/memoria, estas funciones devuelven un iterador en lugar de una lista.

Mapa

La función mapa nos permite aplicar una función a cada elemento en un objeto iterable. Por ejemplo, si tuviéramos una lista de nombres y quisiéramos agregar un saludo a las cadenas, podemos hacer lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
names = ['Shivani', 'Jason', 'Yusef', 'Sakura']
greeted_names = map(lambda x: 'Hi ' + x, names)

# This prints something similar to: <map object at 0x10ed93cc0>
print(greeted_names)
# Recall, that map returns an iterator 

# We can print all names in a for loop
for name in greeted_names:
    print(name)

Filtro

La función filter prueba cada elemento en un objeto iterable con una función que devuelve Verdadero o Falso, manteniendo solo aquellos que se evalúan como Verdadero. Si tuviéramos una lista de números y quisiéramos quedarnos con los que son divisibles por 5 podemos hacer lo siguiente:

1
2
3
4
5
numbers = [13, 4, 18, 35]
div_by_5 = filter(lambda num: num % 5 == 0, numbers)

# We can convert the iterator into a list
print(list(div_by_5)) # [35]

Combinando mapa y filtro

Como cada función devuelve un iterador, y ambos aceptan objetos iterables, ¡podemos usarlos juntos para algunas manipulaciones de datos realmente expresivas!

1
2
3
4
# Let's arbitrarily get the all numbers divisible by 3 between 1 and 20 and cube them
arbitrary_numbers = map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))

print(list(arbitrary_numbers)) # [27, 216, 729, 1728, 3375, 5832]

La expresión en números_arbitrarios se puede dividir en 3 partes:

  • range(1, 21) es un objeto iterable que representa números de 1, 2, 3, 4... 19, 20.
  • filter(lambda num: num % 3 == 0, range(1, 21)) es un iterador para la secuencia numérica 3, 6, 9, 12, 15 y 18.
  • Cuando están elevados al cubo por la expresión mapa podemos obtener un iterador para la secuencia numérica 27, 216, 729, 1728, 3375 y 5832.

Comprensiones de lista

Una característica popular de Python que aparece de manera destacada en los lenguajes de programación funcional es la comprensión de listas. Al igual que las funciones mapa y filtro, las listas de comprensión nos permiten modificar datos de una manera concisa y expresiva.

Probemos nuestros ejemplos anteriores con mapa y filtro con listas de comprensión en su lugar:

1
2
3
4
5
6
# Recall
names = ['Shivani', 'Jan', 'Yusef', 'Sakura']
# Instead of: map(lambda x: 'Hi ' + x, names), we can do
greeted_names = ['Hi ' + name for name in names]

print(greeted_names) # ['Hi Shivani', 'Hi Jason', 'Hi Yusef', 'Hi Sakura']

Una lista básica de comprensión sigue este formato: [resultado para elemento singular en nombre-lista].

Si queremos filtrar objetos, entonces necesitamos usar la palabra clave if:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Recall
numbers = [13, 4, 18, 35]
# Instead of: filter(lambda num: num % 5 == 0, numbers), we can do
div_by_5 = [num for num in numbers if num % 5 == 0]

print(div_by_5) # [35]

# We can manage the combined case as well:
# Instead of: 
# map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))
arbitrary_numbers = [num ** 3 for num in range(1, 21) if num % 3 == 0]
print(arbitrary_numbers) # [27, 216, 729, 1728, 3375, 5832]

Cada expresión mapa y filtro se puede expresar como una lista de comprensión.

Algunas cosas a considerar {#algo a considerar}

Es bien sabido que el creador de Python, Guido van Rossum, no tenía la intención de que Python tuviera características funcionales, pero apreció algunos de los beneficios que su introducción ha traído al lenguaje. Discutió la historia de las características del lenguaje de programación funcional en una de sus [publicaciones de blog] (http://python-history.blogspot.com/2009/04/origins-of-pythons-funcional-features.html). Como resultado, las implementaciones del lenguaje no se han optimizado para las funciones de programación funcional.

Además, la comunidad de desarrolladores de Python no fomenta el uso de la amplia gama de funciones de programación funcional. Si estuviera escribiendo código para que lo revisara la comunidad global de Python, escribiría listas de comprensión en lugar de usar mapa o filtro. Lambdas se usaría mínimamente como nombraría sus funciones.

En tu intérprete de Python, ingresa import this y verás "The Zen of Python". Python generalmente fomenta que el código se escriba de la manera más obvia posible. Idealmente, todo el código debe escribirse de una manera: la comunidad no cree que deba estar en un estilo funcional.

Conclusión

La programación funcional es un paradigma de programación con software compuesto principalmente de funciones que procesan datos a lo largo de su ejecución. Aunque no existe una definición única de lo que es la programación funcional, pudimos examinar algunas características destacadas de los lenguajes funcionales: funciones puras, inmutabilidad y funciones de orden superior.

Python nos permite codificar en un estilo funcional y declarativo. Incluso tiene soporte para muchas características funcionales comunes como Lambda Expressions y las funciones mapa y filtro.

Sin embargo, la comunidad de Python no considera el uso de técnicas de Programación Funcional como la mejor práctica en todo momento. Aun así, hemos aprendido nuevas formas de resolver problemas y, si es necesario, podemos resolver problemas aprovechando la expresividad de la Programación Funcional. .

Licensed under CC BY-NC-SA 4.0