Una función juega el mismo rol que un ladrillo en la construcción de una casa: is a building block for a program.
Permite crear dos tipos de abstracciones:
Process abstraction: permite olvidarnos del proceso, abstraernos del algoritmo que está detrás del resultado de la función
Task decomposition: permite particionar un projecto en varias partes. Divide and conquer.
Functional programming: es otro paradigma de la programación basado en funciones de una sola variable
Call - Define
Son los dos elementos claves para comprender a una función:
- Llamar a una función: a function call
- Definir a una función: como establecemos el proceso/ el algoritmo que realiza una función.
Calls
# Function call
print("Daniel")
# Function call with annotation
resultado : int = round(88.5)
resultado
Hemos incluido aquí las anotaciones que surgen del Syntax for Variable Annotations:
El objetivo es especificar el tipo de objeto que estoy creando.
Observar que el function call
implica los ()
Un objeto se dice callable
cuando podemos ejecutarlo como una función.
Una cuestión importante: no nos interesa lo que está detrás del proceso de rounding
: confiamos que está bien hecho, testeado, etc...
dir(round)
round.__call__(10.5)
El call
es una expresión que se evalua en un resultado de un tipo específico.
Importante: None
es un tipo de resultado, i.e., si no retorna nada, eso es algo.
Define a function.
Define el algoritmo que se ejecuta cuando llamo a la función.
La ejecución se realiza detrás de la escena, no se vé, solo se vé el resultado: process abstraction
3 tipos de definiciones:
- built-ins
- 3rd party functions: import....
- user functions
Problema: queremos sortar un regalo con tirando un dado.
Buscamos y encontramos que podemos hacerlo con el método randint
del modulo random
Esto es:
## Veamos un ejemplo del segundo tipo
from random import randint
roll: int = randint(1, 6)
print(roll)
Observación
El proceso anterior nos permite abstraernos del proceso que genera el resultado.
Sin embargo, tengamos en cuenta esta analogía:
1.- Cuando buscamos restaurante vamos a TripAdvisor
.
2.- Cuando compramos un coche te segunta mano, vamos a Carfax
Por tanto, si es la primera vez que usamos una función que no conocemos, deberíamos:
Informarnos, al menos, por encima de cuales son las alternativas a la que queremos utilizar
Ventajas e inconvenientes de la función-modulo-paquete que quiero utilizar
Veremos ahora
La sintáxis
Construir nuestras propias funciones definidas por el usuario.
El objetivo es ganar comprensión lectora
Problema: tenemos una lista de nombres y queremos encontrar los nombres que empiezan por la letra M.
First, solve the problem. Then, write the code
Esto implica necesariamente distinguir entre:
- Conocimientos técnicos: resolución del problema
- Concimientos de programación o código.
Por tanto, lo primero que vamos a hacer es resolver el problema en un papel en blanco.
¿Cómo puedo destructurar el problema en una serie de pasos que me lleve a la solución?
En otras palabras, cómo puedo construir un algoritmo redactado con mis palabras, no en la sintáxis del programa
Mi pseudo-código casero:
1.- Mirar el primer nombre de la lista.
2.- Mirar a la primera letra.
3.- Si la primera letra de ese nombre es la que me interesa, guardo el nombre.
4.- Si no es, sigo al siguiente nombre.
5.- Repito puntos 2-4 con los hasta que se agoten los nombres.
lista_de_nombres = ['Alejandra', 'Manuela', 'Pedro', 'manuel', 'Jose']
# Mirar el primer nombre
print(lista_de_nombres[0])
# Ver si la primera letra
print(lista_de_nombres[0][0])
# Ver si la primera letra es la que me interesa
print(ord(lista_de_nombres[0][0]) == ord("M"))
# Repetir
lista_nombres_M = []
for nombre in lista_de_nombres:
if nombre[0] == "M":
lista_nombres_M.append(nombre)
print(lista_nombres_M)
# Ver que hay un problema: no da manuel, con minsucula. Posible solución:
# usar alguna función que normalize la mayuscula/minuscula: str.upper()
Problema: esto no es generalizable a cualqueir otra lista
Una función permite generalizar la solución a cualquier tipo de lista o letra del nombre que me interese.
El principio de reusabilidad: si tenemos un fragmento de código usado en muchos sitios, la mejor solución sería pasarlo a una función.
El principio de modularidad: en vez de escribir largos trozos de código, es mejor crear módulos o funciones que agrupen ciertos fragmentos de código en funcionalidades específicas, haciendo que el código resultante sea más fácil de leer.
Como en mátematicas, ua función tendrá un nombre, parametros, un código a ejecutar y un output, e.g.,
```python def funcion(x): codigo return
def nombres_primera_letra(lista, letra):
"""Obtener nombres de una lista que empiezan por una letra"""
lista_nombres = []
for nombre in lista:
if nombre[0].upper() == letra.upper():
lista_nombres.append(nombre)
return lista_nombres
nombres_primera_letra.__doc__
# Veamos si funciona
nombres_primera_letra(lista_de_nombres, "M")
# La ventaja de una función es que podemos generalizar su uso
nombres_primera_letra(lista_de_nombres, "A")
Ejercicio: Defina una función que retorne el máximo de dos números
# Posible solución
def mi_maximo(a: float, b:int) -> float:
"""Obtengo el máximo de dos números"""
if a > b:
return print(f"El número a: {a} es mayor que b: {b}")
elif a < b:
return print(f"El número a: {a} es menor que el número b: {b}")
else:
return print(f"Los números a: {a} y b: {b} son iguales")
Function annotations: PEP 3107
# Descomentar
mi_maximo(5,4)
#mi_maximo(5,4.3)
Type Checker: testear si las annotaciones son correctas
mi_maximo.__doc__
En Python, cuyo lema es que otro entienda lo que hago, hay reglas para escribir y presentar funciones
El Style general está expuesto en PEP 8 Style Guide y, en lo que respecta a funciones
Veamos cómo escribir una una función simple en Python, i.e., $f(x) = 2 x + 1$
Function defition: is a signature
a contract
La definición de una función es un function definition statement
: el interprete lo mira y dice, aquí viene una bloque relacionado con una función.
def mi_maximo(a, b):
pass
Name
: el nombre es lo que utilizarmos para llamar la funcion: my_maximo
Parameters
: es el nombre de las variables que son el input de la función, en la definición de la función, a
y b
`Arguments: es la información enviada a la función cuando se la llama
mi_maximo(5,4)
return
: devuelve un objeto, the type of the return. A None
is an object. Por defecto, devuelve None
Pasemos entonces a definir la función anterior:
def punto_en_recta(x: float) -> float:
"""Ecuacion de la recta"""
return 2 * x + 1
print(punto_en_recta(5.5))
Es importante observar la sintáxis en esta definición: aquí tenemos un identation
, cuatro espacios desde el borde.
Observar que en R
, la sintáxis es distinta
recta <- function(x) {
r <- 2*x + 1
return(r)
}
pero la idea es la misma.
Para llamar una función hacemos un call
.
Cuestión: solo podemos hacer un call
en objetos callables
, i.e., funciones o métodos.
5(1)
Queremos una función que, como resultado, reporte el saldo de un deposito un depósito bancario al cabo de un número de meses en función de un tipo de interés.
def rendimiento(saldo, tipo_i, meses):
rend = saldo *((1+ tipo_i/meses)**meses)
return rend
Hay que tener cuidado con los identations
:
def abs_function(x):
if x < 0:
abs_value = -x
else:
abs_value = x
return abs_value
abs_function(-4)
Una función puede tener un número arbitrario de declaraciones return
, incluso ningúna declaración de return
.
La ejecución de la función termina cuando se alcanza el primer retorno, lo que permite código como el siguiente ejemplo
def f(x):
if x < 0:
return 'negativo'
return 'nonegativo'
Las funciones sin declaración de retorno devuelven automáticamente el objeto especial de Python None
.
def f(x):
a = 2*x
a = f(2)
type(a)
Convertir el script siguiente en una función con tres parámetros: el número de observaciones, la media y la varianza de la distribución normal.
La función on debe tener ningún retorno, solamente reportar el gráfico.
Importante: cuando algo no se conoce o no se entiende, se pregunta en la red.
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot()
n = 100
e_values = [] # empty list
for i in range(n):
e = np.random.normal(0,1,1)
e_values.append(e)
plt.plot(e_values)
plt.show()
def graf(n,m,v):
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot()
e_values = [] # empty list
for i in range(n):
e = np.random.normal(m,v,1)
e_values.append(e)
plt.plot(e_values)
plt.show()
graf(1000, 1,1)
1.- Utilizando el script anterior, escriba una función para la evolución de la inflación como una forma autorregresiva:: $$ \pi_{t} = \alpha \times \pi_{t-1} + e_{t} $$ donde $e_{t} \sim N(m,v)$ y $x_0 = z$
2.- Que pueda observar, en un solo gráfico, cómo la inflación futura depende del nivel de distintos $\alpha's$.
def st(n,a,m,v,x0):
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot()
x = np.empty([n,len(a)])
x[0] = x0
m, v = 0, 1
for c, alfa in enumerate(a):
for i in range(1,n):
x[i,c] = alfa * x[i-1,c] + np.random.normal(m,v,1)
ax.plot(x[:,c], label = f"$\\alpha$={alfa}")
ax.set_xlabel("Tiempo")
ax.set_ylabel("Tasa de inflación")
plt.legend()
plt.title("Evolución de $x_t = \\alpha \\times x_{t-1} + \epsilon_t$")
plt.show()
st(n = 100,a = [0, 0.5, 1],m = 0,v = 1,x0 = 0.1)
Los argumentos por posición o posicionales son la forma más básica e intuitiva de pasar parámetros.
Buscar en google:
matplotlib lines line2D
Tener en cuenta que el objetivo es alcanzar una comprensión lectora: no memorizar
En la siguiente función, los argumentos para los parámetros a,b
son posicionales: importa su posición.
def add_arg(a,b):
c = a + b
return c
add_arg(5,4)
# Otra forma de llamar a la función con argumentos
x = (5,4)
add_arg(*x)
El operador *
Se utiliza en muchos contextos: unpacking, regular expressions, etc..
En funciones hace referencia a unpacking
a = (1,2,3)
print(a)
print(*a)
a = {"a":1, "b": 2}
print(*a)
# Unpacking
a, *b = [1,2,3,4,5]
#a, *_, b = [1,2,3,4,5]
print(*b)
Volvamos a los argumentos posicionales
El orden en el que se introducen los argumentos posicionales importa
# El orden importa
def add_arg(a,b):
c = a.upper()
d = np.log(b)
print(c)
return d
x = ("Juan",4)
#x = (4, "Juan")
add_arg(*x)
Por defecto, una funcion retorna `None`
def add_arg(b,c):
d = b + c
a = add_arg(1,1)
print(a)
The positional arguments are bound, assinged, first.
Vamos ahora a incluir default arguments (or keyword arguments)
Otra forma de llamar a una función, es usando el nombre del argumento con = y su valor.
def f(x, a=1, b=1):
return a + b * x
f(1)
def f(a=1, b=1, x):
return a + b * x
f(1)
Naturalmente, podemos modificar los argumentos por defecto
f(2, a=4, b=5)
Otra forma de llamarlo, introducir los argumentos, sería con un diccionario, e.g., **kwargs
d = {"a": 4, "b": 5}
f(2, **d)
Podemos definir funciones que tengan un número no determinado de argumentos.
Por ejemplo una suma de todos los elementos que querramos
def suma(numeros):
total = 0
for n in numeros:
total += n
return total
suma([1,2,3,4]*10)
#print([1,2,3,4]*2)
La sintaxis adecuada en Python para incluir parámetros de longitud variable
def f(*args,**kwargs):
pass
Variadic positional arguments
*args
La sintaxis especial *args
en las definiciones de función en Python se utiliza para pasar un número variable de argumentos a una función.
def media(*args):
return sum(args) / len(args)
v = (1,2,3,4,5,6,7,8,9,10)
media(*v)
Observar el orden que se insertan los argumentos
def function(*args, j, g):
print(args, j, g)
values = [1, 2, 3, 4]
function(j="Hola j", g=" Hola g", *values)
function(j="Hola j", *values, g=" Hola g")
function(*values, j="Hola j", g=" Hola g")
Pero:
Como son argumentos posicionales, la primera y segunda llamada no pude hacerse con argumentos explícitos, i.e.
function(j="Hola j", g=" Hola g", 1, 2, 3, 4)
function(j="Hola j", 1, 2, 3, 4, g=" Hola g")
ya que daría un error posicional
SyntaxError: positional argument follows keyword argument
**kwargs
La sintaxis especial **kwargs
en las definiciones de funciones en Python se utiliza para pasar una lista de argumentos de longitud variable con palabras clave.
def ciudades(**kwargs):
for clave, valor in kwargs.items():
print(f"{clave.capitalize()}: {valor}")
datos_1 = {"ciudad": "Montevideo",
"coordenadas": "Lat Long",
"población": 1.5}
ciudades(**datos_1)
datos_2 = {"ciudad": "Vigo",
"coordenadas": "Lat Long",
"población": 0.5,
"ideoma": "gallego"}
ciudades(**datos_2)
Orden de los argumentos:
Postitional, Default, *args, **kwargs
Puede colocar *args
en cualquier lugar de la lista de parámetros. Sin embargo, cualquier parámetro definido después de *args
debe especificarse utilizando el formato de palabra clave nombre_parámetro=valor al llamar a la función.
Un parámetro prefijado con **
sólo puede definirse al final de la lista de parámetros.
Si incluimos un *
en la definicion de la función, todos los argumentos posteriores se requiere incluirlos com names values:
def muchos_argumentos(*, a, b,c,d):
def add_arg(*,b,c):
d = b + c
return d
add_arg(b = 1,c = 1)
add_arg(1,1)
1.- Default arguements
2.- Scope
3.- Closures
4.- Decorators
5.- Lambda functions
6.- Partial https://www.thepythoncodingstack.com/p/pythons-functools-partial-and-partialmethod
Las funciones de Python son muy flexibles.
En particular
Daremos ejemplos de lo sencillo que es pasar una función a una función en las secciones siguientes.
Las funciones definidas por el usuario son importantes para mejorar la claridad de su código al
(Escribir algo dos veces es casi siempre una mala idea)
Veamos como convertir en funciones lo que hemos visto en la clase anterior
ts_length = 100
e_values = [] # empty list
for i in range(ts_length):
e = np.random.randn()
e_values.append(e)
plt.plot(e_values)
plt.show()
def generate_data(n):
e_values = []
for i in range(n):
e = np.random.randn()
e_values.append(e)
return e_values
data = generate_data(100)
plt.plot(data)
plt.show()
Cuando el intérprete llega a la expresión generar_datos(100)
, ejecuta el cuerpo de la función con n
igual a 100. El resultado neto es que el nombre datos
está ligado a la lista e_valores
devuelta por la función.
El resultado neto es que el nombre datos
está ligado a la lista e_valores
devuelta por la función.
Nuestra función generar_datos()
es bastante limitada.
Vamos a hacerla un poco más útil dándole la capacidad de devolver normales estándar o variables aleatorias uniformes en $ (0, 1) $ según sea necesario.
Esto se logra en la siguiente pieza de código.
def generate_data(n, generator_type):
e_values = []
for i in range(n):
if generator_type == 'U':
e = np.random.uniform(0, 1)
else:
e = np.random.randn()
e_values.append(e)
return e_values
data = generate_data(100, 'U')
plt.plot(data)
plt.show()
Afortunadamente, la sintaxis de la cláusula if/else se explica por sí misma, y la sangría delimita de nuevo la extensión de los bloques de código.
Notas
U
como una cadena, por lo que lo escribimos como 'U'
. ==
, no =
. a = 10
asigna el nombre a
al valor 10
. a == 10
se evalúa como Verdadero
o Falso
, dependiendo del valor de a
. Hay varias formas de simplificar el código anterior.
Por ejemplo, podemos deshacernos de los condicionales simplemente pasando el tipo de generador deseado como una función.
Para entender esto, considere la siguiente versión.
def generate_data(n, generator_type):
e_values = []
for i in range(n):
e = generator_type()
e_values.append(e)
return e_values
data = generate_data(100, np.random.uniform)
plt.plot(data)
plt.show()
Ahora, cuando llamamos a la función generate_data()
, pasamos np.random.uniform
como segundo argumento.
Este objeto es una función.
Cuando se ejecuta la llamada a la función generar_datos(100, np.random.uniform)
, Python ejecuta el bloque de código de la función con n
igual a 100 y el nombre tipo_generador generator_type
"ligado" a la función np.random.uniform
.
generator_type
y np.aleatorio.uniforme
son "sinónimos", y pueden usarse de forma idéntica. Este principio funciona de forma más general: por ejemplo, considere el siguiente fragmento de código
max(7, 2, 4) # max() is a built-in Python function
m = max
m(7, 2, 4)
Aquí creamos otro nombre para la función incorporada max()
, que luego podría
utilizarse de la misma forma.
En el contexto de nuestro programa, la capacidad de vincular nuevos nombres a funciones significa que no hay problema en pasar una función como argumento a otra función-como hicimos anteriormente.
La variable aleatoria binomial $ Y \sim Bin(n, p) $ representa el número de aciertos en $ n $ ensayos binarios, donde cada ensayo tiene éxito con probabilidad $ p $.
Sin ninguna importación aparte de from numpy.random import uniform
, escribe una función que reporte un número sorteado entre $1$ y $n$, el número asignado a cada participantes. En otras palabras, sorteo_binomial_rv(n, p)
debe reportar el número sorteado, i.e., el valor de $ Y $.
Ayuda: si $ U $ es uniforme en $ (0, 1) $ y la probabilidad, $ p \in [0,1] $, entonces la expresión U < p
se evalúa como Verdadero
con probabilidad $ p $.
from numpy.random import uniform
def binomial_rv(n, p):
count = 0
for i in range(n):
U = uniform()
if U < p:
count = count + 1 # Or count += 1
return count
binomial_rv(10, 0.5)
En primer lugar, escriba una función que devuelva una realización del siguiente dispositivo aleatorio
k
veces dentro de la misma secuencia, al menos un vez, paga un dólar. from numpy.random import uniform
def secuencia_para_pago(k, n): # paga is salen k exitos consecutivos
pago = 0
count = 0
for i in range(n):
U = uniform()
count = count + 1 if U < 0.5 else 0
print(U,count) # print counts
if count == k:
pago = 1
return pago
secuencia_para_pago(3, 10)
Ahora escriba otra función que realice la misma tarea excepto que la segunda regla del dispositivo aleatorio anterior pasa a ser
k
o más veces dentro de esta secuencia, paga un dólar. No uses ninguna importación aparte de from numpy.random import uniform
.
def secuencia_para_pago(k, n): # paga si al menos k exitos
pago = 0
count = 0
for i in range(n):
U = uniform()
count = count + ( 1 if U < 0.5 else 0 )
print(count)
if count == k:
payoff = 1
return payoff
secuencia_para_pago(3, 10)