Clase 7: Funciones, decoradores y programación funcional
Hemos visto que en Python todo es un objeto, con lo cual, incluso las funciones, son objetos. Como tales tienen métodos y atributos:
lio_messi = "Lio Messi"
print(type(lio_messi))
<class 'str'>
La variable lio_messi
es un string, y como tal, pertenece a la clase
str
, que tiene sus propios atributos y métodos:
Veamos qué pasa con las funciones:
def saluda_a(alguien):
saludo = f"Hola {alguien}!"
return saludo
print(saluda_a(lio_messi))
Hola Lio Messi!
print(type(saluda_a))
<class 'function'>
Un atributo interesante de las funciones es __name__
por razones
que veremos en breve:
print(saluda_a.__name__)
saluda_a
Es decir, __name__
es el nombre de la función, que está guardado
dentro del objeto que representa dicha función. > La capacidad del
lenguaje de responderse preguntas sobre las propias entidades que
componen el lenguaje se llama introspección.
Clases atrás vimos dos características importantes de las funciones en Python. La primera de ellas es que las funciones pueden retornar (esto es, crear) otras funciones:
def genera_recta(a,b):
"Genera la función recta y = a x + b"
def recta(x):
"Evalúa la función recta en x"
y = a * x + b
return y
return recta
f = genera_recta(2,3) # f(x) = 2 * x + 3
x = 2
print(f"f({x}) = {f(x)}") # f(2) = 2 * 2 + 3
x = 0
print(f"f({x}) = {f(x)}") # f(0) = 2 * 0 + 3
f(2) = 7
f(0) = 3
print(type(f))
<class 'function'>
La segunda de ellas es que es posible pasar como argumento una función a otra:
g = genera_recta(1,-1) # g(x) = x - 1
x = 3
y = f(g(x))
print(f"y = {y}")
y = 7
print(type(g))
<class 'function'>
Funciones que aceptan y devuelven funciones (Decoradores)
Vamos a trabajar ahora con los decoradores. Los decoradores no son otra cosa que funciones, pero que, por sus características, adquieren ese nombre y una forma particular de llamarlos que reduce convenientemente la sintaxis al programar. Empecemos por definir una función que devuelve otra función, como vimos arriba, de la siguiente forma:
def mi_decorador(func):
def wrapper():
print(f"Por llamar a la función {func.__name__}")
func()
print(f"Listo, ya llamé a la función {func.__name__}")
return wrapper
Definamos ahora un saludo genérico:
def saluda():
print("Holaa!!")
saluda()
Holaa!!
Nada nuevo hasta ahora, pero empecemos a combinar las funciones:
saluda_w = mi_decorador(saluda)
saluda_w()
Por llamar a la función saluda
Holaa!!
Listo, ya llamé a la función saluda
print(type(saluda_w))
<class 'function'>
Tenemos ahora una función saluda
y su versión decorada
saluda_w
, que simplemente llama a la función saluda
, pero además
imprime mensajes antes y después del llamado a la función. Esto es algo
que uno va a querer hacer, por ejemplo para calcular el tiempo de
ejecución de una función, o para imprimir mensajes de registro
(logging) o debug, u otras tantas cosas más. Por eso Python introduce
una notación especial para este tipo de funciones mi_decorador
:
@mi_decorador
def saluda_en_ingles():
print("Hello!!")
Notar que el decorador siempre empieza con el símbolo ``@`` y se
encuentra en la línea inmediatamente anterior a la definición de la
función.
saluda_en_ingles()
Por llamar a la función saluda_en_ingles
Hello!!
Listo, ya llamé a la función saluda_en_ingles
Qué pasa si queremos aplicar el decorador a una función que recibe
argumentos como saluda_a
?
@mi_decorador
def saluda_a(alguien):
print(f"Hola {alguien}!")
saluda_a("Lio Messi")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[20], line 1
----> 1 saluda_a("Lio Messi")
TypeError: mi_decorador.<locals>.wrapper() takes 0 positional arguments but 1 was given
Notemos que como está definido el decorador, recibe una función sin argumentos:
def mi_decorador(func):
def wrapper():
print(f"Por llamar a la función {func.__name__}")
func()
print(f"Listo, ya llamé a la función {func.__name__}")
return wrapper
En este último caso, al aplicar @mi_decorador
a
saluda_a(alguien)
, estamos pasando a la función mi_decorador
una
función func
que dentro de mi_decorador
se llama como
func()
, es decir, no tiene argumentos. Para resolver este problema,
tenemos que indicar explícitamente que la función que vamos a llamar
dentro del decorador puede tener argumentos:
def mi_nuevo_decorador(func):
def wrapper(*args, **kwargs):
print(f"Por llamar a la función {func.__name__}")
func(*args, **kwargs)
print(f"Listo, ya llamé a la función {func.__name__}")
return wrapper
@mi_nuevo_decorador
def saluda_a(alguien):
print(f"Hola {alguien}!")
saluda_a("Lio Messi")
Por llamar a la función saluda_a
Hola Lio Messi!
Listo, ya llamé a la función saluda_a
Hasta ahora la función func
que envuelve el decorador no devuelve
ningún valor, sólo imprime un mensaje en pantalla. Cómo hacemos para
usar un decorador con una función que devuelve un valor?
def proto_debug_decorator(func):
def wrapper(*args, **kwargs):
print(f"Por llamar a la función {func.__name__}")
resultado = func(*args, **kwargs)
print(f"Listo, ya llamé a la función {func.__name__}")
return resultado
return wrapper
@proto_debug_decorator
def mi_calculo_complicado(x,y,z=0):
return x**2 + y**2 + z**2
v = mi_calculo_complicado(1,2,3)
print(v)
Por llamar a la función mi_calculo_complicado
Listo, ya llamé a la función mi_calculo_complicado
14
Decoradores, un ejemplo más útil
Recordemos que al llamar una función, *args
representa a la tupla de
argumentos mientras que **kwargs
es el diccionario de argumentos
opcionales. Escribamos un par de funciones útiles para transformar estos
tipos en string, de modo que se puedan imprimir, por ejemplo:
def args_as_str(*args, **kwargs):
args_str = ", ".join([str(a) for a in args])
kwargs_str = ", ".join([f"{k}={v}" for k,v in kwargs.items()])
return f"{args_str}, {kwargs_str}"
def debug_me(func):
def wrapper(*args, **kwargs):
print(f"{func.__name__} ({args_as_str(*args, **kwargs)})")
resultado = func(*args, **kwargs)
print(f"Listo, ya llamé a la función {func.__name__}")
return resultado
return wrapper
@debug_me
def mi_calculo_recontracomplicado(x,y,z=0):
return x**2 + y**2 + z**2
v = mi_calculo_recontracomplicado(1,2,z=3)
print(v)
mi_calculo_recontracomplicado (1, 2, z=3)
Listo, ya llamé a la función mi_calculo_recontracomplicado
14
Ejercicios 07 (a)
El módulo time calcula el tiempo en segundos desde el comienzo de la era de la computación (?), que para los fines prácticos, da inicio el 1 de enero de 1970 ;-D. Veamos unos ejemplos de su uso:
import time
ahora = time.time()
print (ahora)
# duerme 5 segundos
time.sleep(5) # zzzz.....
ahora = time.time()
print (ahora)
1709038864.1231098
1709038869.1235564
Utilizando las funciones anteriores, escriba el decorador @time_me
que calcula e imprime el tiempo que tarda en ejecutarse una función.
No empiece desde cero!! Use como plantilla para empezar el decorador
@debug_me
y modifíquelo adecuadamente.
# descomente el decorador una vez que lo tenga programado
# @time_me
def mi_calculo_recontralargo(n):
l= [x for x in range(n)]
return sum(l)
mi_calculo_recontralargo(20000000)
199999990000000
Programación funcional con Python
La programación funcional es un paradigma de programación, de la misma manera que otros paradigmas, como la programación orientada a objetos, o la programación estructurada.
Existen lenguajes de programación que son directamente funcionales, esto es, implementan las reglas de la programación funcional directamente (por ejemplo, Lisp, Haskell, F#, etc.). Desde un punto de vista histórico, la programación funcional tiene su origen en la visión de Alonzo Church del problema de la decisión (Entscheidungsproblem), y es complementaria a la más conocida, propuesta por Alan Turing.
Python es un lenguaje orientado a objetos (todo elemento del lenguaje es un objeto), de modo tal que no es posible hablar de un paradigma funcional en Python, sino mas bien de un estilo de programación funcional.
Un trabajo interesante es el siguiente: ’Why Functional Programming Matters: http://www.cse.chalmers.se/~rjmh/Papers/whyfp.pdf”.
Los errores al programar
En el continuo devenir de la programación, uno se encuentra, principalmente, resolviendo errores. Un resumen de los errores posibles en un código se pueden encontrar en la expresión
i = i+1
En esta expresión podemos encontrar tres tipos de errores:
Error de lectura : el valor de
i
en el lado derecho no es el que efectivamente uno desearía, es decir, el código está leyendo un valor incorrecto.Error de escritura : el valor de
i
en el lado izquierdo no es el que efectivamente uno desearía, es decir, estamos guardando la expresión en una variable incorrecta.Error de cómputo : que se produce, por ejemplo, porque no queremos sumar 1 sino 2, o queremos restar el valor de i.
Existe un cuarto tipo de error que aparece y tiene que ver con un error de flujo, en el cual el código se ejecuta en una rama que no es la deseada, debido a que una condición lógica no se cumple tal como se esperaba. O por ejemplo, el orden en que se ejecutan las sentencias no es el adecuado:
# Función que calcula (x+1)(x+2)
def f(x):
x = x+1
y = x+1
return x*y
# Función que calcula (x+1)(x+2) ?? Mmmm.....
def g(x):
y = x+1
x = x+1
return x*y
print(f(3))
print(g(3))
20
16
Los errores en notebooks
Además de las complejidades propias de la programación, que están asociadas al dominio donde se encuentra el problema que uno quiere resolver, y a las dificultades que eso implica; los notebooks introducen también una dificultad adicional: uno puede redefinir los datos en celdas posteriores, pero puede volver ‘atrás’ en el código y recalcular otra celda. Veamos un ejemplo:
data = [1,2,3,4]
def prom(a):
s = sum(a)
n = len(a)
return s/n
prom(data)
2.5
data = "Some data"
print(len(data))
9
Mutabilidad
Los problemas que vemos arriba se deben a la mutabilidad: las variables pueden cambiar (esto es, ser reescritas) a lo largo del código. Ahora bien, pareciera que la mutabilidad es intrínseca a la computación, al fin y al cabo, en el hardware hay una cantidad limitada de memoria y de registros que son continuamente reescritos para que nuestro código corra. Sin embargo, los lenguajes de programación de alto nivel que usamos nos alejan (afortunadamente) del requerimiento de mantener el estado de la memoria y los registros explícitamente en el código (y en el algoritmo en nuestra cabeza).
La pregunta que cabe entonces es ¿cómo hacer un código que prevenga la mutabilidad, pero que a la vez me permita transformar los datos para resolver mi problema? La respuesta viene de la mano de un ente muy conocido en mátemáticas: las funciones
Funciones
Una función desde el punto de vista matemático es una relación que a cada elemento de un conjunto le asocia exactamente un elemento de otro conjunto. Estos conjuntos pueden ser números, vectores, matrices en el mundo matemático,
\(y = f(x)\)
\(y = f(x)\)
o, en un mundo más físico, peras, manzanas, nombres, apellidos, objetos varios:
Estas funciones tienen dos características fundamentales para usar en programación: - Permiten “transformar” un valor en otro - El valor original no se modifica
Es decir que el uso de funciones, al estilo matemático, en un código resuelven el problema de la mutabilidad, pero a la vez me permiten “transformar”, es decir, crear nuevos valores a partir del valor original.
Funciones puras
El análogo computacional de las funciones matemáticas se llaman funciones puras. Una función se dice pura cuando: - Siempre retorna el mismo valor de salida para el mismo valor de entrada - No tiene efectos colaterales (side effects)
Funciones de primer orden o primera clase
Un lenguaje se dice que tiene funciones de primera clase cuando son tratadas exactamente igual que otros valores o variables.
Funciones de orden superior
Un lenguaje que permite pasar funciones como argumentos se dice que acepta funciones de orden superior.
def square(x):
return x*x
def next(x):
return x+1
a = 4
b = next(a)
c = next(next(a))
print(a)
print(b)
print(c)
4
5
6
def h(x):
return (next(x))*(next(next(x)))
print(h(3))
20
Si se tiene funciones puras, es posible componerlas
def compose(f, g):
return lambda x: f(g(x))
next2 = compose(next,next)
print(next2(a))
6
Inmutabilidad
Usando funciones puras se garantiza la inmutabilidad de los valores hacia adentro de la función. Pero, ¿qué sucede afuera? Python, al no ser un lenguaje funcional per se, no tiene la capacidad de establecer la inmutabilidad de cualquier valor, excepto para los casos de strings y tuplas, además, obviamente, de las expresiones literales.
Queda entonces en el programador la responsabilidad de no mutar los datos…
… o usar anotaciones de tipos
def cube(x: int) -> int:
return x*x*x
print(cube(2))
8
Nótese que Python NO chequea los tipos de datos, no tiene manera en
forma nativa de hacerlo. Por eso puedo ejecutar la función cube
con
floats, por ejemplo:
print(cube(3.0))
27.0
Usando mypy
Para poder utilizar la anotación de tipos en forma efectiva, se puede
recurrir a mypy. Esta es una
aplicación que me permite comprobar tipos de datos anotados en Python.
Para instalar mypy
usamos:
conda install mypy
o lo instalamos a través del manejador de paquetes de preferencia (Anaconda, mamba, etc.)
Podemos ver el ejemplo 07_cube.py
!python3 cube.py
python3: can't open file '/home/fiol/Clases/IntPython/clases-python/clases/cube.py': [Errno 2] No such file or directory
!mypy cube.py
/bin/bash: line 1: mypy: command not found Es posible que uno quiera usarmypy
sobre un archivo de notebookipynb
. Para eso hay que instalar la aplicaciónnbQA
más detalles acá.
Si se usa VS Code, se puede instalar la extensión MyPy Type Checker
,
y luego indicarle a VS Code que la utilice. Para eso vamos a
Settings
y buscamos type checking
. Donde dice PyLance
elegimos el modo que queremos que se chequeen los tipos, desde off
hasta strict
.
No más loops
Si las funciones deben ser puras, y las ‘variables’ dejan de ser
variables y pasan a ser valores, entonces no puede haber loops en mi
código. Un loop necesita invariablemente un contador (i = i+1
) que
necesariamente es una variable mutable. Así que así nomás, de un plumazo
no existen más loops.
¿Entonces? Entonces, todos los loops se reemplazan por llamados a funciones recursivas, o se utilizan funciones de orden superior:
# Filter
l = [1,2,3,4,5,6]
def es_par(x):
return (x%2 == 0)
pares = list(filter(es_par,l))
print(pares)
[2, 4, 6]
# Filter usando list comprehension
list(x for x in l if es_par(x))
[2, 4, 6]
# Map
siguientes = list(map(next,l))
print(siguientes)
[2, 3, 4, 5, 6, 7]
El módulo functools
provee la función reduce
, que complementa a
map
y filter
.
# Reduce
from functools import *
import operator
# Suma usando el predicado desde el módulo `operator`
suma = reduce(operator.add,l,0)
print(suma)
21
help(reduce)
Help on built-in function reduce in module _functools:
reduce(...)
reduce(function, iterable[, initial]) -> value
Apply a function of two arguments cumulatively to the items of a sequence
or iterable, from left to right, so as to reduce the iterable to a single
value. For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates
((((1+2)+3)+4)+5). If initial is present, it is placed before the items
of the iterable in the calculation, and serves as a default when the
iterable is empty.
# Suma usando el predicado como lambda
otra_suma = reduce(lambda x,y: x+y, l)
print(otra_suma)
21
# Suma definiendo la propia función suma
def add(x,y):
return x+y
y_otra_suma = reduce(add,l)
print(y_otra_suma)
21
La suma de los cuadrados de una lista:
suma_cuadrados = reduce(lambda x,y: x+y, map(square,l))
print(suma_cuadrados)
91
Ejercicios 07 (b)
Construya una función
partition(lst,predicate)
que dada una listalst
y una funciónpredicate
, separe la listalst
en dos: una lista que contiene los valores para los cuales la funciónpredicate
devuelveTrue
, y otra lista que contiene los valores para los quepredicate
devuelveFalse
:def is_even(x): return x % 2 == 0 numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] evens, odds = divide_list(numbers, is_even) print(evens) # Output: [2, 4, 6, 8, 10] print(odds) # Output: [1, 3, 5, 7, 9]
Dado la cadena de caracteres
s1='En un lugar de la Mancha de cuyo nombre no quiero acordarme'
Utilice
reduce
,map
y/ofilter
(y las funciones auxiliares necesarias) para:Obtener la cantidad de caracteres.
Imprimir la frase anterior pero con cada palabra empezando en mayúsculas.
Contar cuantas letras ‘a’ tiene la frase.
Contar cuántas vocales tiene.