Programación funcional en Python

P

Introducción

La programación funcional es un paradigma de programación popular estrechamente vinculado a los fundamentos matemáticos de la informática. Si bien no existe una definición estricta de lo que constituye un lenguaje funcional, consideramos que son lenguajes que utilizan 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 declarativo idiomas, le dicen a la computadora qué resultado quieren. Esto suele contrastarse con imperativo idiomas 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 tener una mejor apreciación de lo que es un lenguaje funcional, veamos las características de Haskell que pueden verse como características 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 una vez creados. Tomemos, por ejemplo, la creación de un List con 3 elementos y almacenarlo en una variable my_list. Si my_list es inmutable, no podrá cambiar los elementos individuales. Tendrías que establecer my_list a un nuevo List 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 abstraer acciones, dándonos flexibilidad en el comportamiento de nuestro código.

Haskell también ha influido en los 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 ninguna característica o biblioteca especial 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 la misma salida cada vez que ejecutamos la función con la misma entrada.

Creemos una función pura para multiplicar números por 2:

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 numbers no se modifican y no hacemos referencia a ninguna otra variable fuera de la función, por lo que es puro.

Inmutabilidad

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

Python ofrece algunos tipos de datos inmutables, uno popular es el Tuple. Comparemos la tupla con una lista, que es mutable:

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: 'tuple' object does not support item assignment.

Ahora, hay un escenario interesante donde un Tuple puede parecer un objeto mutable. Por ejemplo, si quisiéramos cambiar la lista en immutable_collection de [4, 5] a [4, 5, 6], puede hacer lo siguiente:

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

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

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

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

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:

def write_repeat(message, n):
    for i in range(n):
        print(message)

write_repeat('Hello', 5)

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

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:

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 add5 y add10 funciones, 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:

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, obtenemos un mayor control del comportamiento de nuestro programa.

Python proporciona algunas funciones de orden superior integradas ú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

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

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

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 seguido de los argumentos de la función. Después de los dos puntos está el código devuelto por lambda. Esta capacidad de crear funciones “sobre la marcha” se utiliza 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 desea más información.

Funciones de orden superior integradas

Python ha implementado algunas funciones de orden superior de uso común 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 iterator en lugar de una lista.

Mapa

los map función 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:

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)

Filtrar

los filter La función prueba cada elemento de un objeto iterable con una función que devuelve True o False, solo conservando aquellos que evalúan True. Si tuviéramos una lista de números y quisiéramos mantener aquellos que son divisibles por 5 podemos hacer lo siguiente:

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]

Combinatorio map y filter

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

# 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 arbitrary_numbers se puede dividir en 3 partes:

  • range(1, 21) es un objeto iterable que representa números del 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 en cubos por el map expresión podemos obtener un iterador para la secuencia numérica 27, 216, 729, 1728, 3375 y 5832.

Lista de comprensiones

Una característica popular de Python que aparece de manera destacada en los lenguajes de programación funcionales son las listas por comprensión. Como el map y filter funciones, listas por comprensión nos permiten modificar datos de forma concisa y expresiva.

Probemos nuestros ejemplos anteriores con map y filter con listas por comprensión en su lugar:

# 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 de comprensión básica sigue este formato: [result for singular-element in list-name].

Si nos gustaría filtrar objetos, entonces necesitamos usar el if palabra clave:

# 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 map y filter La expresión se puede expresar como una lista de comprensión.

Algunas cosas 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. Habló de la historia de las características del lenguaje de programación funcional en uno de sus publicaciones de blog. Como resultado, las implementaciones del lenguaje no se han optimizado para las características 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 por comprensión en lugar de usar map o filter. Lambdas se usaría mínimamente como nombraría sus funciones.

En su intérprete de Python, ingrese import this y verá “El Zen de Python”. Python generalmente alienta a 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 tener un estilo funcional.

Conclusión

La programación funcional es un paradigma de programación con software compuesto principalmente por 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 declarativo funcional. Incluso tiene soporte para muchas características funcionales comunes como Lambda Expressions y el map y filter funciones.

Sin embargo, la comunidad de Python no considera que el uso de técnicas de programación funcional sea 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.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad