Python Para Principiantes (X) - Programación funcional

Programación funcional

Índice:

Introducción

Hasta el momento, hemos trabajamos con un paradigma (una forma de modelar y diseñar programas) conocido como programación imperativa.

Este tipo de programación es la hace uso de declaraciones, bucles y funciones.

Recordemos que, al comienzo del taller, habíamos dicho que Python es un lenguaje que soportaba múltiples paradigmas.

Ahora vamos a ingresar a conocer (de manera superficial) un nuevo paradigma que es la Programación Funcional.

¿Qué es la programación funcional?

Es un estilo de programación que hace uso, y se construye, en torno a las funciones.

Estas funciones son conocidas como funciones de alto orden o funciones de orden superior.

La característica que tienen estas funciones de orden superior es la de tomar como argumentos de entrada otras funciones o bien devolver una función como retorno.

1
2
3
4
5
6
7
def dos_veces(funcion, valor):
   return funcion(funcion(valor))

def mas_uno(x):
   return x + 1

print(dos_veces(mas_uno, 10)) #Le estoy enviando a la func dos_veces una función(mas_uno) y un valor(10).

Conociendo a las funciones puras e impuras.

Cuando trabajamos con programación funcional buscamos hacer uso de las funciones puras.

Estas funciones son aquellas retornan un valor, el cual depende de los parámetros de entrada, y no se observan ningún efecto secundario.

1
2
3
#Ejemplo de función pura
def doble_de_la_suma(x,y):
    return (x+y)/2
1
2
3
4
5
6
7
8
#Ejemplo de una función impura
miLista = [0,1]

def funcion_impura(valor):
    miLista.append(valor)

funcion_impura(231)
print(miLista)

Esta función anterior es considerada impura debido a que tiene un efecto secundario. Modifica la lista (miLista).

Lambdas

Se le dice lambda es una función que toma uno o más argumentos pero tiene solo una expresión implementada en ella (siempre se trata de una implementación simple que pueda expresarse en no más de una línea de código.

Estas funciones son anónimas.

Para ir aclarando estos conceptos arrancaremos definiendo una función mediante el operador def como venimos realizando hasta el momento.

1
2
3
4
5
6
#Funcíón que devuelve el doble de un valor
def doble(x):
    doble = x*2
    return doble

print(doble(10)) 

Esta función se puede hacer en menos líneas de código.

1
2
3
4
5
#Funcíón que devuelve el doble de un valor
def doble(x):
    return x*2

print(doble(10)) 

Esta misma función se puede “acomodar” en una sola línea

1
2
3
4
#Funcíón que devuelve el doble de un valor
def doble(x): return x*2

print(doble(10))

Entonces esta función:

1
def doble(x): return x*2

Puede ser escrita en forma de función lambda de la siguiente manera haciendo uso del operador lambda

1
lambda x:x*2

La estructura de una función lambda es la siguiente:

1
lambda argumentos: expresión

donde argumentos son los “valores” con los que la función opera y expresión es la operación o implementación en si misma.

Se suele asignar una función lambda a una variable de la siguiente forma:

1
variable = lambda argumentos: expresión

Veamos un ejemplo práctico

1
2
3
4
#La misma función que venimos trabajando en forma de lambda
#Creamos una variable y le asignamos la función
doble = lambda x:x*2
print(doble(10))

Un ejemplo con dos argumentos

1
2
suma = lambda x,y:x+y #Función que suma dos valores
print(suma(5,10))

Como vimos, las funciones lambda siempre son una expresión que cabe en una línea, nunca un bloque de código. Además, decíamos que eran anónimas, esto se debe a que no tienen un nombre (o identificar) explicito para llamarlas. Dicho de otra manera, son funciones sin nombre.

Mapas

Un mapa es una función que necesita de dos argumentos: una función y un objeto iterable (listas, diccionarios,etc.). El objetivo del mapa es aplicar la función a cada elemento del objeto iterable.

Tiene la siguiente forma.

1
map(función, iterable)

Imaginemos que tengo una lista de números y quiero restarle 1 a cada elemento.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#Lista con valores númericos.
def restar_uno(x):
    return x-1

miLista = [0,1.341, 10, 931, 34.2123]

resultado = map(restar_uno, miLista) #Se va a aplicar la función restar_uno a cada elemento de miLista
print("el resultado es del tipo:",type(resultado)) #Observen que la función map me va a devolver un tipo mapa
resultado = list(resultado) #Convierto el mapa a lista
print(resultado) #Muestro en pantalla

Una buena idea es aplicar el concepto de lambdas al usar mapas

1
2
3
miLista = [0,1.341, 10, 931, 34.2123]
resultado = list(map(lambda x:x-1, miLista)) #Aplico lambda y convierto a lista en una misma linea.
print(resultado)

Filtros

Un filtro es otra función parecida a mapa. Necesita dos argumentos: una función y un iterable. La diferencia radica en que el filtro va a remover elementos que NO cumplan cierta condición, por lo tanto esa función siempre tiene que ser condicional. Tiene la forma:

1
filter(función, iterable)

Imaginemos el siguiente ejemplo:

Dada una lista de números enteros queremos remover aquellos que son pares.

1
2
3
4
5
6
7
8
9
def esPar(x): #Si x es par devuelve True/Verdadero
    if x%2 == 0:
        return True
    
miLista = [7,19,22,10,33,44,110]
resultado = filter(esPar,miLista) #Aplicamos el filtro
print("resultado es del tipo", type(resultado)) #mostrarmos que devuelve un objeto filtro
resultado = list(resultado) #lo convertirmos a una lista
print(resultado)

A los filtros también resulta conveniente escribirlos en lambdas

1
2
3
miLista = [7,19,22,10,33,44,110]
resultado = list(filter(lambda x:x%2==0,miLista)) #Aplicamos el filtro y convertimos a lista en una misma linea
print(resultado)

Generadores

Normalmente al momento de crear una lista la declaramos vacía y luego con un bucle la vamos llenando.

1
2
3
4
5
numeros = [] #Lista vacía
for i in range(10): #Bucle donde i toma valores de 0 a 9
    numeros.append(i) #Agrego el valor de i al final de la lista

print(numeros)

Como se ve en el ejemplo anterior, los valores generados los vamos almacenando en una estructura y una vez finalizado el bucle podemos trabajar con ellos. Los generadores son funciones que nos permiten definir una iteración, por ejemplo una secuencia de 0 a 9, pero de a un elemento a la vez.

1
2
3
4
5
6
7
8
def crear_numeros(x): #Definimos una función
    i = 0
    while i < x:        
        yield i
        i +=1
        
for i in crear_numeros(10):
    print(i)

Podemos usar una función generadora y crear una lista de la siguiente manera

1
2
3
4
5
6
7
8
def crear_numeros(x): #Definimos una función
    i = 0
    while i < x:        
        yield i
        i +=1
        
lista = list(crear_numeros(10))
print(lista)

Dijimos que podemos ir generando la secuencia de uno en uno, esto lo podemos comprobar de la siguiente forma

1
2
3
4
5
6
7
8
9
def crear_numeros(x): #Definimos una función
    i = 0
    while i < x:        
        yield i
        i +=1

numero = crear_numeros(10) #Le asignamos a una variable la función generadora
print(next(numero)) #Mediante el operador next vamos iterando sobre la secuencia definida en la linea anterior (0 a 9)
print(next(numero))

En resumen, los generadores nos permiten declarar una función que se comporta como iterador y puede ser usada en bucle.

La diferencia principal radica en que el generador va a ir cediendo valores en tiempo de ejecución.

El uso de funciones generadoras aumenta la performance del programa y baja el consumo de memoria.

Otra ventaja es que se pueden ir usando los elementos a medida que se van creando.

Decoradores

Son funciones que nos permiten agregar funcionalidades a otra función.

La idea es no modificar la función original.

Ejemplo:

Tenemos la siguiente función que muestra un texto en pantalla

1
2
3
4
def saludo():
    print("Hola alumnes")

saludo()

Ahora bien, queremos hacer de este texto algo “más vistoso” sin modificar la función original.

Para ello usamos las funciones decoradoras.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def decorador(funcion):
    #Declaramos una función 
    def adornos():
        print("************")
        funcion() #En este caso función es igual a saludo()
        print("************")
   
    return adornos  #Retornamos la funcion adornos 

def saludo():
    print("Hola alumnes")
    
saludoDecorado = decorador(saludo) #Asignamos
saludoDecorado() #Invocamos
************
Hola alumnes
************

Hay una forma más simple de invocar a decoradores en Python que es la siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def decorador(funcion):
    #Declaramos una función 
    def adornos():
        print("************")
        funcion() #En este caso función es igual a saludo()
        print("************")
   
    return adornos  #Retornamos la funcion adornos 

@decorador
def saludo():
    print("Hola alumnes")
    
saludo()
************
Hola alumnes
************

Recuerde que una función puede tener múltiples decoradores

Recursividad

La recursividad es un concepto fundamental en la programación funcional.

Hace referencia a funciones que se invocan a si mismas con el fin de resolver algún problema.

Hay un ejemplo clásico en este tema y es el factorial.

1
2
3
4
5
6
7
def factorial(x):
  if x == 1:
    return 1
  else:     
    return x * factorial(x-1)
    
print(factorial(5))
5
4
3
2
120

Las funciones recursivas siempre tienen que tener un caso base, es decir una condición que se cumpla para que se detenga la ejecución de la recursividad.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def fibonacci(n):
    if n < 2:
        return n
    else:
        # fn = fn-1 + fn-2 
        return fib(n-1) + fib(n-2)
    
print(fibonacci(3))
#Suceción de fibonacci es 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 ...

updatedupdated2021-02-032021-02-03