Clase 4: Funciones
Las funciones son objetos
Veamos en Python qué es una función:
sum
<function sum(iterable, /, start=0)>
print
<function print(*args, sep=' ', end='n', file=None, flush=False)>
En Python function
es un objeto, con una operación importante:
podemos llamarla (ejecutarla), en la forma:
func(lista-de-argumentos)
Como con todos los objetos, podemos definir una variable y asignarle una función (algo así como lo que en C sería un puntero a funciones)
En Python, las funciones son de primera clase, es decir que tienen las siguientes propiedades:
Asignación a Variables: Se puede asignar la función a una variable, lo cual permite referenciarla de manera similar a como se haría con un número, cadena de texto u otro tipo de dato. Esto ocurre en Python dado que todo elemento del lenguaje es un objeto.
Pasaje como argumento: Se puede pasar la función como argumento a otras funciones.
Retorno de Funciones: Una función puede devolver otra función como valor de retorno
Almacenamiento en Estructuras de Datos: Se pueden almacenar funciones en estructuras de datos como listas, diccionarios, y otros contenedores.
Veamos algunos ejemplos sencillos:
f = sum
help(f)
Help on built-in function sum in module builtins:
sum(iterable, /, start=0)
Return the sum of a 'start' value (default: 0) plus an iterable of numbers
When the iterable is empty, return the start value.
This function is intended specifically for use with numeric values and may
reject non-numeric types.
a = [1, 2, 3]
print('¿f is sum? ', f is sum)
print('f == sum? ', f == sum)
print('f(a)=', f(a), ' sum(a)=', sum(a))
¿f is sum? True
f == sum? True
f(a)= 6 sum(a)= 6
Un uso bastante común es crear un diccionario donde los valores sean funciones:
funciones = {'suma': sum, 'mínimo': min, 'máximo': max}
funciones['suma']
<function sum(iterable, /, start=0)>
funciones['suma'](a)
6
print(list(funciones.items()))
[('suma', <built-in function sum>), ('mínimo', <built-in function min>), ('máximo', <built-in function max>)]
print('\n', 'a =', a,'\n')
for k, v in funciones.items():
print(k,"=", v(a))
a = [1, 2, 3] suma = 6 mínimo = 1 máximo = 3 En Python, se estila que las funciones así como las variables estén escritas en minúsculas y usando snake case, es decir, usando un guión bajo ’_’ para separar las palabras que constituyen el nombre de la función (o de la variable)
mi_variable = 3
primos = [2, 3, 5, 7]
def promedio(l):
v = sum(l)/len(l)
return v
mi_variable = 3
primos = [2, 3, 5, 7]
def promedio(l):
v = sum(l)/len(l)
return v
promedio(primos)
4.25
Definición básica de funciones
Ya vimos en la segunda clase como definir una función. Consideremos una función para calcular la velocidad y altura de una pelota en caída libre: Para dar mejor funcionalidad, le damos la posibilidad al usuario de dar la posición y la velocidad iniciales como argumento:
g = 9.8 # aceleración de la gravedad en m/s^2
def caida(t, h_0, v_0):
"""Calcula la velocidad y posición de una partícula a tiempo t, para condiciones iniciales dadas
h_0 es la altura inicial (en m)
v_0 es la velocidad inicial (en m/s)
Se utiliza el valor de aceleración de la gravedad g=9.8 m/s^2
"""
v = v_0 - g*t
h = h_0 - v_0*t - g*t**2/2.
return v,h
help(caida)
Help on function caida in module __main__:
caida(t, h_0, v_0)
Calcula la velocidad y posición de una partícula a tiempo t, para condiciones iniciales dadas
h_0 es la altura inicial (en m)
v_0 es la velocidad inicial (en m/s)
Se utiliza el valor de aceleración de la gravedad g=9.8 m/s^2
t = 2.2
v0 = 12
alt = 100
v,h = caida(t, alt, v0)
print(f'''Para caída desde {alt}m, con vel. inicial {v0}m/s, a t = {t},
la velocidad será v={v:6.3}, y estará a una altura de {h:7.4}m''')
Para caída desde 100m, con vel. inicial 12m/s, a t = 2.2,
la velocidad será v= -9.56, y estará a una altura de 49.88m
Notemos que podemos llamar a esta función de varias maneras. Podemos llamarla con la constante, o con una variable indistintamente. En este caso, el argumento está definido por su posición. Luego siguen dos argumentos nombrados, la altura inicial (h_0) y el tercero corresponde a la velocidad inicial (v_0).
v0 = 12
caida(2.2, 100, v0)
(-9.560000000000002, 49.883999999999986)
Pero en Python podemos usar el nombre de la variable en el llamado a la función. Por ejemplo:
caida(v_0=v0,t=2.2, h_0=100)
(-9.560000000000002, 49.883999999999986)
Funciones con argumentos opcionales
Las funciones pueden tener muchos argumentos. En Python pueden tener un número variable de argumentos y pueden tener valores por default para algunos de ellos. En el caso de la función de caída libre, vamos a extenderlo de manera que podamos usarlo fuera de la tierra (o en otras latitudes) permitiendo cambiar el valor de la gravedad y asumiendo que, a menos que lo pidamos explícitamente se trata de una simple caída libre:
def caida_libre(t, h0, v0=0., g=9.8):
"""Devuelve la velocidad y la posición de una partícula en
caída libre para condiciones iniciales dadas
Parameters
----------
t : float
el tiempo al que queremos realizar el cálculo
h0: float
la altura inicial
v0: float (opcional)
la velocidad inicial (default = 0.0)
g: float (opcional)
valor de la aceleración de la gravedad (default = 9.8)
Returns
-------
(v,h): tuple of floats
v= v0 - g*t
h= h0 - v0*t -g*t^2/2
"""
v = v0 - g*t
h = h0 - v0*t - g*t**2/2.
return v,h
# Desde 1000 metros con velocidad inicial cero
print( caida_libre(2, 1000))
# Desde 1000 metros con velocidad inicial hacia arriba
print(caida_libre(1, 1000, 10))
# Desde 1000 metros con velocidad inicial cero
print(caida_libre(h0=1000, t=2))
# Desde 1000 metros con velocidad inicial cero en la luna
print( caida_libre(v0=0, h0=1000, t=14.2857137))
# Desde 1000 metros con velocidad inicial cero en la luna
print( caida_libre(v0=0, h0=1000, t=14.2857137, g=1.625))
print(caida_libre(14.2857137, 1000, 0, 1.625))
(-19.6, 980.4)
(0.1999999999999993, 985.1)
(-19.6, 980.4)
(-139.99999426000002, 8.199999820135417e-05)
(-23.2142847625, 834.1836870663262)
(-23.2142847625, 834.1836870663262)
help(caida_libre)
Help on function caida_libre in module __main__:
caida_libre(t, h0, v0=0.0, g=9.8)
Devuelve la velocidad y la posición de una partícula en
caída libre para condiciones iniciales dadas
Parameters
----------
t : float
el tiempo al que queremos realizar el cálculo
h0: float
la altura inicial
v0: float (opcional)
la velocidad inicial (default = 0.0)
g: float (opcional)
valor de la aceleración de la gravedad (default = 9.8)
Returns
-------
(v,h): tuple of floats
v= v0 - g*t
h= h0 - v0*t -g*t^2/2
Nota
No se pueden usar argumentos con nombre antes de los
argumentos requeridos (en este caso t
).
Tampoco se pueden usar argumentos sin su nombre después de haber incluido alguno con su nombre. Por ejemplo no son válidas las llamadas:
caida_libre(t=2, 0.)
caida_libre(2, v0=0., 1000)
caida_libre(t=2, 0.)
Cell In[20], line 1
caida_libre(t=2, 0.)
^
SyntaxError: positional argument follows keyword argument
caida_libre(2, v0=0., 1000)
Cell In[21], line 1
caida_libre(2, v0=0., 1000)
^
SyntaxError: positional argument follows keyword argument
Número variable de argumentos y argumentos keywords
Se pueden definir funciones que toman un número variable de argumentos
(como una lista), o que aceptan un diccionario como argumento. Este tipo
de argumentos se llaman argumentos keyword (kwargs
). Una buena
explicación se encuentra en el Tutorial de la
documentación.
Ahora vamos a dar una explicación rápida. Consideremos la función f
,
que imprime sus argumentos:
def f(p, *args, **kwargs):
print( f"p : {p}, tipo: {type(p)}")
print( f"args : {args}, tipo: {type(args)}")
print( f"kwargs: {kwargs}, tipo: {type(kwargs)}")
f(1)
p : 1, tipo: <class 'int'>
args : (), tipo: <class 'tuple'>
kwargs: {}, tipo: <class 'dict'>
f(1,2,3)
p : 1, tipo: <class 'int'>
args : (2, 3), tipo: <class 'tuple'>
kwargs: {}, tipo: <class 'dict'>
f(1,2,3,4,5,6)
p : 1, tipo: <class 'int'>
args : (2, 3, 4, 5, 6), tipo: <class 'tuple'>
kwargs: {}, tipo: <class 'dict'>
En este ejemplo, el primer valor se asigna al argumento requerido p
,
y los siguientes a una variable que se llama args
, que es del tipo
tuple
f(1.5, 2, 3, 5, anteultimo = 9, ultimo = -1)
p : 1.5, tipo: <class 'float'>
args : (2, 3, 5), tipo: <class 'tuple'>
kwargs: {'anteultimo': 9, 'ultimo': -1}, tipo: <class 'dict'>
f(1, (1,2,3), 4, ultimo=-1)
p : 1, tipo: <class 'int'>
args : ((1, 2, 3), 4), tipo: <class 'tuple'>
kwargs: {'ultimo': -1}, tipo: <class 'dict'>
En estas otras llamadas a la función, todos los argumentos que se pasan indicando el nombre se asignan a un diccionario.
Al definir una función, con la construcción *args
se indica “mapear
todos los argumentos posicionales no explícitos a una tupla llamada
``args``”. Con **kwargs
se indica “mapear todos los argumentos de
palabra clave no explícitos a un diccionario llamado kwargs
”. Esta
acción de convertir un conjunto de argumentos a una tuple o diccionario
se conoce como empacar o empaquetar los datos.
Nota
Por supuesto, no es necesario utilizar los nombres “args” y
“kwargs”. Podemos llamarlas de cualquier otra manera! los simbolos que
indican cantidades arbitrarias de parametros son *
y **
. Además
es posible poner parametros “comunes” antes de los parametros
arbitrarios, como se muestra en el ejemplo.
Un ejemplo de una función con número variable de argumentos puede ser la
función multiplica
:
def multiplica(*args):
s = 1
for a in args:
s *= a
return s
multiplica(2,5)
10
multiplica(2,3,5,9,4)
1080
Ejercicios 4 (a)
Defina una función que dada una altura inicial
h_0
, una velocidad inicialv_0
y un paso de tiempodelta_t
, devuelva:una lista conteniendo los tiempos de cálculo,
otra lista con las posiciones h(t) (alturas) de la partícula en función del tiempo.
una tercer lista con las velocidades v(t) de la partícula en cada instante.
Use las funciones definidas en la clase
Escriba una función
imprime_trayectoria
que, obviamente, imprime la trayectoria h(t) en forma clara y bella. Para esto puede resultarle útil la función`zip
<https://docs.python.org/3.3/library/functions.html#zip>`__.
Argumentos de las funciones
Ámbito de las variables en los argumentos
Consideremos la siguiente función
sep = 47*'-'
def func1(x):
print(sep,'\nx entró a la función con el valor', x)
print('Id adentro:',id(x))
x = 2
print('El nuevo valor de x es', x)
print('Id adentro nuevo:',id(x),'\n', sep)
x = 50
print('Id afuera antes:',id(x))
print('Fuera de la función: Originalmente x vale',x)
func1(x)
print('Fuera de la función: Ahora x vale',x)
print('Id afuera después:',id(x))
Id afuera antes: 140624857736656
Fuera de la función: Originalmente x vale 50
-----------------------------------------------
x entró a la función con el valor 50
Id adentro: 140624857736656
El nuevo valor de x es 2
Id adentro nuevo: 140624857735120
-----------------------------------------------
Fuera de la función: Ahora x vale 50
Id afuera después: 140624857736656
Vemos que la variable x
que utilizamos como argumento de la función
debe ser diferente a la variable x
que se define fuera de la
función, ya que su valor no cambia al salir. Además usamos la función
id()
para obtener la identidad de la variable. Python pasa las
variables como referencia al objeto que representa a la variable. En
este caso el objeto que estamos pasando es un entero que es inmutable,
con lo cual en la función misma se crea un nuevo objeto y se reasigna la
referencia al nuevo objeto. De este modo, no se afecta al objeto
original.
Consideremos ahora la siguiente función:
def func2(x):
print(sep,'\nx entró a la función con el valor', x)
print('Id adentro:',id(x))
x = [2,7]
print('El nuevo valor de x es', x)
print('Id adentro nuevo:',id(x),'\n', sep)
La función es muy parecida, sólo que le estamos dando a la variable
x
dentro de la función un nuevo valor del tipo lista
.
x = 50
print('Id afuera antes:',id(x))
print('Fuera de la función: Originalmente x vale',x)
func2(x)
print('Fuera de la función: Ahora x vale',x)
print('Id afuera después:',id(x))
Id afuera antes: 140624857736656
Fuera de la función: Originalmente x vale 50
-----------------------------------------------
x entró a la función con el valor 50
Id adentro: 140624857736656
El nuevo valor de x es [2, 7]
Id adentro nuevo: 140624567783040
-----------------------------------------------
Fuera de la función: Ahora x vale 50
Id afuera después: 140624857736656
x = [50]
print('Id afuera antes:',id(x))
print('Fuera de la función: Originalmente x vale',x)
func2(x)
print('Fuera de la función: Ahora x vale',x)
print('Id afuera después:',id(x))
Id afuera antes: 140624567781568
Fuera de la función: Originalmente x vale [50]
-----------------------------------------------
x entró a la función con el valor [50]
Id adentro: 140624567781568
El nuevo valor de x es [2, 7]
Id adentro nuevo: 140624567780544
-----------------------------------------------
Fuera de la función: Ahora x vale [50]
Id afuera después: 140624567781568
¿Qué está pasando acá?
Cuando se realiza la llamada a la función, se le pasa una referencia del nombre
x
.Cuando le damos un nuevo valor dentro de la función, como en el caso
x = [2, 7]
, entonces se crea una nueva variable y el nombrex
queda asociado a la nueva variable.La variable original –definida fuera de la función– no cambia.
En el primer caso, como los escalares son inmutables (de la misma manera que los strings y tuplas) no puede ser modificada. Al reasignar el nombre siempre se crea una nueva variable (para cualquier tipo).
Consideremos estas variantes, donde el comportamiento entre tipos mutables e inmutables son diferentes:
def func3(x):
print(sep,'\nx entró a la función con el valor', x)
print('Id adentro:',id(x))
x += [2,7]
print('El nuevo valor de x es', x)
print('Id adentro nuevo:',id(x),'\n', sep)
x = [50]
print('Id afuera antes:',id(x))
print('Fuera de la función: Originalmente x vale',x)
func3(x)
print('Fuera de la función: Ahora x vale',x)
print('Id afuera después:',id(x))
Id afuera antes: 140624567742592
Fuera de la función: Originalmente x vale [50]
-----------------------------------------------
x entró a la función con el valor [50]
Id adentro: 140624567742592
El nuevo valor de x es [50, 2, 7]
Id adentro nuevo: 140624567742592
-----------------------------------------------
Fuera de la función: Ahora x vale [50, 2, 7]
Id afuera después: 140624567742592
En este caso, como no estamos redefiniendo la variable sino que la estamos modificando, el nuevo valor se mantiene al terminar la ejecución de la función. Otra variante:
def func4(x):
print(sep,'\nx entró a la función con el valor', x)
print('Id adentro:',id(x))
x[0] = 2
print('El nuevo valor de x es', x)
print('Id adentro nuevo:',id(x),'\n', sep)
x = [50]
print('Id afuera antes:',id(x))
print('Fuera de la función: Originalmente x vale',x)
func4(x)
print('Fuera de la función: Ahora x vale',x)
print('Id afuera después:',id(x))
Id afuera antes: 140624567782208
Fuera de la función: Originalmente x vale [50]
-----------------------------------------------
x entró a la función con el valor [50]
Id adentro: 140624567782208
El nuevo valor de x es [2]
Id adentro nuevo: 140624567782208
-----------------------------------------------
Fuera de la función: Ahora x vale [2]
Id afuera después: 140624567782208
Vemos que, cuando modificamos la variable (solo es posible para tipos mutables), asignando un valor a uno o más de sus elementos o agregando/removiendo elementos, la copia sigue apuntando a la variable original y el valor de la variable, definida originalmente afuera, cambia.
Tipos mutables en argumentos opcionales
Hay que tener cuidado cuando usamos valores por defecto con tipos que pueden modificarse dentro de la función. Consideremos la siguiente función:
def func2b(x1, x=[]):
print('x entró a la función con el valor', x)
x.append(x1)
print('El nuevo valor de x es', x)
func2b(1)
x entró a la función con el valor []
El nuevo valor de x es [1]
func2b(2)
x entró a la función con el valor [1]
El nuevo valor de x es [1, 2]
El argumento opcional x
tiene como valor por defecto una lista
vacía, entonces esperaríamos que el valor de x
sea igual a x1
, y
en este caso imprima “El nuevo valor de x es [2]”. Sin embargo, entre
llamadas mantiene el valor de x
anterior. El valor por defecto se
fija en la definición y en el caso de tipos mutables puede modificarse.
Ejercicios 4 (b)
Escriba funciones para analizar la divisibilidad de enteros:
La función
es_divisible
que retornaTrue
si el argumentox
es divisible por alguno de los elemntos del segundo argumento:divisores
. El argumentodivisores
es opcional y si está presente es una variable del tipo lista que contiene los valores para los cuáles debemos examinar la divisibilidad.El valor por defecto de
divisores
es[2,3,5,7]
. Las siguientes expresiones deben retornar el mismo valor:es_divisible(x) es_divisible(x, [2,3,5,7])
La función
es_divisible_por
cuyo primer argumento (mandatorio) esx
, y luego puede aceptar un número indeterminado de argumentos:es_divisible_por(x) # retorna verdadero siempre es_divisible_por(x, 2) # verdadero si x es par es_divisible_por(x, 2, 3, 5, 7) # igual resultado que es_divisible(x) es_divisible_por(x, 2, 3, 5, 7, 9, 11, 13) # o cualquier secuencia de argumentos debe funcionar
PARA ENTREGAR. Describimos una grilla de sudoku como un string de nueve líneas, cada una con 9 números, con números entre 1 y 9. Escribir un conjunto de funciones que permitan chequear si una grilla de sudoku es correcta. Para que una grilla sea correcta deben cumplirse las siguientes condiciones
Los números están entre 1 y 9
En cada fila no deben repetirse
En cada columna no deben repetirse
En todas las regiones de 3x3 que no se solapan, empezando de cualquier esquina, no deben repetirse
Escribir una función que convierta un string con formato a una lista bidimensional. El string estará dado con nueve números por línea, de la siguiente manera (los espacios en blanco en cada línea pueden variar):
sudoku = """145327698 839654127 672918543 496185372 218473956 753296481 367542819 984761235 521839764"""
Escribir una función
check_repetidos()
que tome por argumento una lista (unidimensional) y devuelva verdadero si la lista tiene elementos repetidos y falso en caso contrario.Escribir la función
check_sudoku()
que toma como argumento una grilla (como una lista bidimensional de9x9
) y devuelva verdadero si los números corresponden a la resolución correcta del Sudoku y falso en caso contrario. Note que debe verificar que los números no se repiten en filas, ni en columnas ni en recuadros de3x3
. Para obtener la posición de los recuadros puede investigar que hacen las líneas de código:
j, k = (i // 3) * 3, (i % 3) * 3 r = [grid[a][b] for a in range(j, j+3) for b in range(k, k+3)]
suponiendo que
grid
es el nombre de nuestra lista bidimensional, cuandoi
toma valores entre0
y8
.
Nota
Enviar por correo electrónico con el archivo adjunto nombrado en la forma
04_Apellido.py
donde “Apellido” es su apellido.
Anotaciones de tipos
El lenguaje Python provee tipos dinámicos de datos. Esto quiere decir que el intérprete define los tipos al momento de ejecutar código, por consiguiente, no soporta (ni requiere) la anotación de tipos en el código. Los tipos dinámicos otorgan al programador la facilidad de introducir variable y mutarlas de tipo sin inconvenientes. Por otra parte, hemos visto que las funciones definidas en Python pueden tener argumentos opcionales, característica del lenguaje que se usa extensivamente en los módulos y bibliotecas. La combinación de tipos dinámicos y argumentos opcionales implica la consulta frecuente a la documentación para poder encontrar las mejores alternativas para el uso de código ya establecido. La introducción de IDEs poderosos hace que uno pueda consultar dicha documentación mientras programa, pero, a su vez, debemos introducir documentación exhaustiva para poder reutilizarlo.
A partir de la versión 3.5 de Python, y con el objetivo de proveer claridad en el código y hacerlo menos propenso a errores, se introdujo el módulo typing para poder anotar los tipos de datos. En versiones más nuevas (3.9+), la anotación de tipos está incorporada en el intérprete.
Atención!: La anotación de tipos no es usada por el intérprete de Python, ni implica ninguna comprobación previa al momento de ejecución del código. Los IDE actuales sí reconocen las anotaciones e indican los posibles problemas, si se configuran adecuadamente.
La aplicación MyPy puede comprobar los tipos de datos de Python y declarar como error alguna incompatibilidad entre los mismos en el código.
i: int = 1
x: float = 1.0
b: bool = True
s: str = "test"
bt: bytes = b"test"
print(f"{i} de tipo {type(i)}")
print(f"{x} de tipo {type(x)}")
print(f"{b} de tipo {type(b)}")
print(f"{s} de tipo {type(s)}")
print(f"{bt} de tipo {type(bt)}")
l: list[int] = [1]
st: set[int] = {-1,1}
d: dict[str, float] = {"versión": 2.0}
t: tuple[int, str, float] = (10, "Messi", 7.5)
ti: tuple[int, ...] = (1, 2, 3)
print(f"{l} de tipo {type(l)}")
print(f"{st} de tipo {type(st)}")
print(f"{d} de tipo {type(d)}")
print(f"{t} de tipo {type(t)}")
print(f"{ti} de tipo {type(ti)}")
1 de tipo <class 'int'>
1.0 de tipo <class 'float'>
True de tipo <class 'bool'>
test de tipo <class 'str'>
b'test' de tipo <class 'bytes'>
[1] de tipo <class 'list'>
{1, -1} de tipo <class 'set'>
{'versión': 2.0} de tipo <class 'dict'>
(10, 'Messi', 7.5) de tipo <class 'tuple'>
(1, 2, 3) de tipo <class 'tuple'>
Nota
En versiones anteriores a Python 3.8 es necesario importar el
módulo typing
, y los tipos de datos son los mismos pero utilizando
mayúsculas.
En muchas bibliotecas y módulos se sigue utilizando typing
para
proveer compatibilidad con versiones anteriores de Python
from typing import List, Set, Dict, Tuple, Any # Python 3.8 y anteriores
# Para colecciones en versiones de Python 3.9 y posteriores, el tipo de colección a utilizar se escribe entre []
l: List[int] = [1]
st: Set[int] = {-1,1}
d: Dict[str, float] = {"versión": 2.0}
t: Tuple[int, str, float] = (10, "Messi", 7.5)
ti: Tuple[int, ...] = (1, 2, 3)
mx: List[Any] = [1, 1.0, True, "test", b"test", [1], {-1,1}, {"versión": 2.0}, (10, "Messi", 7.5), (1, 2, 3)]
print(f"{l} de tipo {type(l)}")
print(f"{st} de tipo {type(st)}")
print(f"{d} de tipo {type(d)}")
print(f"{t} de tipo {type(t)}")
print(f"{ti} de tipo {type(ti)}")
print(f"{mx} de tipo {type(mx)}")
[1] de tipo <class 'list'>
{1, -1} de tipo <class 'set'>
{'versión': 2.0} de tipo <class 'dict'>
(10, 'Messi', 7.5) de tipo <class 'tuple'>
(1, 2, 3) de tipo <class 'tuple'>
[1, 1.0, True, 'test', b'test', [1], {1, -1}, {'versión': 2.0}, (10, 'Messi', 7.5), (1, 2, 3)] de tipo <class 'list'>
La posibilidad de anotar tipos provee la facilidad de establecer nuevos tipos de datos propios:
from typing import NewType
User = NewType("User", str)
user: User = User("Messi")
print(f"{user} de tipo {type(user)}")
Messi de tipo <class 'str'>
def hola_usuario(usuario: User) -> None:
print(f"Hola {usuario}")
hola_usuario(4)
Hola 4
También se pueden crear alias de tipos
Vector = Tuple[float, float]
Vector3D = Tuple[float, float, float]
origen: Vector = (0.0, 0.0)
origen3D: Vector3D = (0.0, 0.0, 0.0)
print(f"{origen} de tipo {type(origen)}")
(0.0, 0.0) de tipo <class 'tuple'>
Funciones
La sintaxis para anotar los tipos de las funciones es la siguiente:
def suma(a: Vector, b: Vector) -> Vector:
return (a[0] + b[0], a[1] + b[1])
def producto_escalar(a: Vector, b: Vector) -> float:
return a[0] * b[0] + a[1] * b[1]
p1 = (1.0, 2.0)
p2 = (2.0, 1.0)
print(suma(p1, p2))
print(producto_escalar(p1, p2))
(3.0, 3.0)
4.0
El tipo Union
El tipo Union
se utiliza para indicar que una variable puede aceptar
dos o más tipos de datos:
from typing import Union
def suma_numeros(a: Union[int, float], b: Union[int, float]) -> Union[int, float]:
return a + b
print(suma_numeros(1, 2))
print(suma_numeros(1.0, 2.0))
print(suma_numeros(1, 2.0))
3
3.0
3.0
Pero recordemos que Python NO hace ningún chequeo de tipos!
print(suma_numeros("1", "2"))
12
Es responsabilidad del programador observar y hacer observar que los tipos sean compatibles, o usar mypy para chequearlos
El tipo Optional
Como su nombre lo indica, Optional
indica que una variable puede
tener un determinado tipo, o puede ser ‘None’. Es muy útil para anotar
argumentos de funciones que pueden ser, digamos, opcionales.
Optional[<tipo>]
es equivalente aUnion[<tipo>,None]
En Python 3.10 y superiores, se puede usar el operador ‘|’ para indicar una unión
from typing import Optional
s: Optional[str] = None
print(s)
s = "Hola!"
print(s)
def saluda(nombre: Optional[str] = None) -> str:
if nombre:
return f"Hola {nombre}"
else:
return "Hola Mundo"
print(saluda())
print(saluda("Messi"))
None
Hola!
Hola Mundo
Hola Messi
# En Python 3.10+
s: str | None = None # Union[str, None]
print(s)
s = "Hola!"
print(s)
def saluda(nombre: str | None = None) -> str:
if nombre:
return f"Hola {nombre}"
else:
return "Hola Mundo"
print(saluda())
print(saluda("Messi"))
None
Hola!
Hola Mundo
Hola Messi
Control de tipos estáticos
Como vimos la clase anterior, es posible utilizar anotaciones de tipos para mejorar la legibilidad de nuestro código, proveer información sobre los tipos de las variables que usamos y los tipos de retorno de las funciones. Hemos remarcado también que el interprete de Python per se no controla los tipos de datos que estamos utilizando.
Si se desea realizar este control, la herramienta adecuada es MyPy. MyPy es una aplicación que corre además de python para realizar este chequeo de tipos.
La instalamos con
conda install -c conda-forge mypy
!mypy --help
usage: mypy [-h] [-v] [-V] [more options; see below] [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...] Mypy is a program that will type check your Python code. Pass in any files or folders you want to type check. Mypy will recursively traverse any provided folders to find .py files: $ mypy my_program.py my_src_folder For more information on getting started, see: - https://mypy.readthedocs.io/en/stable/getting_started.html For more details on both running mypy and using the flags below, see: - https://mypy.readthedocs.io/en/stable/running_mypy.html - https://mypy.readthedocs.io/en/stable/command_line.html You can also use a config file to configure mypy instead of using command line flags. For more details, see: - https://mypy.readthedocs.io/en/stable/config_file.html options: --enable-incomplete-feature {InlineTypedDict,PreciseTupleTypes} Enable support of incomplete/experimental features for early preview Optional arguments: -h, --help Show this help message and exit -v, --verbose More verbose messages -V, --version Show program's version number and exit -O, --output FORMAT Set a custom output format Config file: Use a config file instead of command line arguments. This is useful if you are using many flags or want to set different options per each module. --config-file CONFIG_FILE Configuration file, must have a [mypy] section (defaults to mypy.ini, .mypy.ini, pyproject.toml, setup.cfg, ~/.config/mypy/config, ~/.mypy.ini) --warn-unused-configs Warn about unused '[mypy-<pattern>]' or '[[tool.mypy.overrides]]' config sections (inverse: --no-warn-unused-configs) Import discovery: Configure how imports are discovered and followed. --no-namespace-packages Disable support for namespace packages (PEP 420, __init__.py-less) (inverse: --namespace-packages) --ignore-missing-imports Silently ignore imports of missing modules --follow-untyped-imports Typecheck modules without stubs or py.typed marker --follow-imports {normal,silent,skip,error} How to treat imports (default normal) --python-executable EXECUTABLE Python executable used for finding PEP 561 compliant installed packages and stubs --no-site-packages Do not search for installed PEP 561 compliant packages --no-silence-site-packages Do not silence errors in PEP 561 compliant installed packages --junit-format {global,per_file} If --junit-xml is set, specifies format. global: single test with all errors; per_file: one test entry per file with failures Platform configuration: Type check code assuming it will be run under certain runtime conditions. By default, mypy assumes your code will be run using the same operating system and Python version you are using to run mypy itself. --python-version x.y Type check code assuming it will be running on Python x.y --platform PLATFORM Type check special-cased code for the given OS platform (defaults to sys.platform) --always-true NAME Additional variable to be considered True (may be repeated) --always-false NAME Additional variable to be considered False (may be repeated) Disallow dynamic typing: Disallow the use of the dynamic 'Any' type under certain conditions. --disallow-any-expr Disallow all expressions that have type Any --disallow-any-decorated Disallow functions that have Any in their signature after decorator transformation --disallow-any-explicit Disallow explicit Any in type positions --disallow-any-generics Disallow usage of generic types that do not specify explicit type parameters (inverse: --allow-any-generics) --disallow-any-unimported Disallow Any types resulting from unfollowed imports (inverse: --allow- any-unimported) --disallow-subclassing-any Disallow subclassing values of type 'Any' when defining classes (inverse: --allow-subclassing-any) Untyped definitions and calls: Configure how untyped definitions and calls are handled. Note: by default, mypy ignores any untyped function definitions and assumes any calls to such functions have a return type of 'Any'. --disallow-untyped-calls Disallow calling functions without type annotations from functions with type annotations (inverse: --allow-untyped-calls) --untyped-calls-exclude MODULE Disable --disallow-untyped-calls for functions/methods coming from specific package, module, or class --disallow-untyped-defs Disallow defining functions without type annotations or with incomplete type annotations (inverse: --allow-untyped-defs) --disallow-incomplete-defs Disallow defining functions with incomplete type annotations (while still allowing entirely unannotated definitions) (inverse: --allow-incomplete- defs) --check-untyped-defs Type check the interior of functions without type annotations (inverse: --no-check-untyped-defs) --disallow-untyped-decorators Disallow decorating typed functions with untyped decorators (inverse: --allow-untyped-decorators) None and Optional handling: Adjust how values of type 'None' are handled. For more context on how mypy handles values of type 'None', see: https://mypy.readthedocs.io/en/stable/kinds_of_types.html#no-strict-optional --implicit-optional Assume arguments with default values of None are Optional (inverse: --no- implicit-optional) --no-strict-optional Disable strict Optional checks (inverse: --strict-optional) Configuring warnings: Detect code that is sound but redundant or problematic. --warn-redundant-casts Warn about casting an expression to its inferred type (inverse: --no-warn- redundant-casts) --warn-unused-ignores Warn about unneeded '# type: ignore' comments (inverse: --no-warn-unused- ignores) --no-warn-no-return Do not warn about functions that end without returning (inverse: --warn- no-return) --warn-return-any Warn about returning values of type Any from non-Any typed functions (inverse: --no-warn-return-any) --warn-unreachable Warn about statements or expressions inferred to be unreachable (inverse: --no-warn-unreachable) --report-deprecated-as-note Report importing or using deprecated features as notes instead of errors (inverse: --no-report-deprecated-as-note) Miscellaneous strictness flags: --allow-untyped-globals Suppress toplevel errors caused by missing annotations (inverse: --disallow-untyped-globals) --allow-redefinition Allow unconditional variable redefinition with a new type (inverse: --disallow-redefinition) --no-implicit-reexport Treat imports as private unless aliased (inverse: --implicit-reexport) --strict-equality Prohibit equality, identity, and container checks for non-overlapping types (inverse: --no-strict-equality) --extra-checks Enable additional checks that are technically correct but may be impractical in real code. For example, this prohibits partial overlap in TypedDict updates, and makes arguments prepended via Concatenate positional-only (inverse: --no-extra-checks) --strict Strict mode; enables the following flags: --warn-unused-configs, --disallow-any-generics, --disallow-subclassing-any, --disallow-untyped- calls, --disallow-untyped-defs, --disallow-incomplete-defs, --check- untyped-defs, --disallow-untyped-decorators, --warn-redundant-casts, --warn-unused-ignores, --warn-return-any, --no-implicit-reexport, --strict-equality, --extra-checks --disable-error-code NAME Disable a specific error code --enable-error-code NAME Enable a specific error code Configuring error messages: Adjust the amount of detail shown in error messages. --show-error-context Precede errors with "note:" messages explaining context (inverse: --hide- error-context) --show-column-numbers Show column numbers in error messages (inverse: --hide-column-numbers) --show-error-end Show end line/end column numbers in error messages. This implies --show- column-numbers (inverse: --hide-error-end) --hide-error-codes Hide error codes in error messages (inverse: --show-error-codes) --show-error-code-links Show links to error code documentation (inverse: --hide-error-code-links) --pretty Use visually nicer output in error messages: Use soft word wrap, show source code snippets, and show error location markers (inverse: --no- pretty) --no-color-output Do not colorize error messages (inverse: --color-output) --no-error-summary Do not show error stats summary (inverse: --error-summary) --show-absolute-path Show absolute paths to files (inverse: --hide-absolute-path) Incremental mode: Adjust how mypy incrementally type checks and caches modules. Mypy caches type information about modules into a cache to let you speed up future invocations of mypy. Also see mypy's daemon mode: mypy.readthedocs.io/en/stable/mypy_daemon.html#mypy-daemon --no-incremental Disable module cache (inverse: --incremental) --cache-dir DIR Store module cache info in the given folder in incremental mode (defaults to '.mypy_cache') --sqlite-cache Use a sqlite database to store the cache (inverse: --no-sqlite-cache) --cache-fine-grained Include fine-grained dependency information in the cache for the mypy daemon --skip-version-check Allow using cache written by older mypy version --skip-cache-mtime-checks Skip cache internal consistency checks based on mtime Advanced options: Debug and customize mypy internals. --pdb Invoke pdb on fatal error --show-traceback, --tb Show traceback on fatal error --raise-exceptions Raise exception on fatal error --custom-typing-module MODULE Use a custom typing module --old-type-inference Disable new experimental type inference algorithm --custom-typeshed-dir DIR Use the custom typeshed in DIR --warn-incomplete-stub Warn if missing type annotation in typeshed, only relevant with --disallow-untyped-defs or --disallow-incomplete-defs enabled (inverse: --no-warn-incomplete-stub) --shadow-file SOURCE_FILE SHADOW_FILE When encountering SOURCE_FILE, read and type check the contents of SHADOW_FILE instead. Report generation: Generate a report in the specified format. --any-exprs-report DIR --cobertura-xml-report DIR --html-report DIR --linecount-report DIR --linecoverage-report DIR --lineprecision-report DIR --txt-report DIR --xml-report DIR --xslt-html-report DIR --xslt-txt-report DIR Miscellaneous: --junit-xml JUNIT_XML Write junit.xml to the given file --find-occurrences CLASS.MEMBER Print out all usages of a class member (experimental) --scripts-are-modules Script x becomes module x instead of __main__ --install-types Install detected missing library stub packages using pip (inverse: --no- install-types) --non-interactive Install stubs without asking for confirmation and hide errors, with --install-types (inverse: --interactive) Running code: Specify the code you want to type check. For more details, see mypy.readthedocs.io/en/stable/running_mypy.html#running-mypy --explicit-package-bases Use current directory and MYPYPATH to determine module names of files passed (inverse: --no-explicit-package-bases) --exclude PATTERN Regular expression to match file names, directory names or paths which mypy should ignore while recursively discovering files to check, e.g. --exclude '/setup.py$'. May be specified more than once, eg. --exclude a --exclude b -m, --module MODULE Type-check module; can repeat for more modules -p, --package PACKAGE Type-check package recursively; can be repeated -c, --command PROGRAM_TEXT Type-check program passed in as string files Type-check given files or directories Environment variables: Define MYPYPATH for additional module search path entries. Define MYPY_CACHE_DIR to override configuration cache_dir path. Recuerde que uno puede acceder a todos los comandos de la terminal usando el símbolo de exclamación como hicimos arriba. También puede ejecutar el comando en una terminal como
mypy --help
Veamos cómo funciona mypy
como aplicación. Para ello ejecutaremos el
programa mypy_ejemplo.py
que contiene este código:
def cube(x: int) -> int:
return x * x * x
def Hola(name: str) -> str:
return "Hola " + name
def Hola2(name):
return "Hola " + name
if __name__ == "__main__":
a = cube(2)
print(f"El cubo de 2 es: {a}")
b = cube(3.0) # Esto no da error en Python, mypy si lo captura
print(f"El cubo de 3.0 es: {b}")
print(f"Hola('Juan'): {Hola('Juan')}")
print(f"Hola2(3): {Hola2(3)}") # Esto da un error de concatenación
print(
f"Hola(3): {Hola(3)}"
) # Esto da un error de concatenación, y además mypy lo captura
Puede copiar el código de arriba y pegarlo en un archivo
mypy_ejemplo.py
, o descargarlo desde (acá)[].
Probemos primero corriendo el código con python
. > Reemplace
../scripts/mypy_example.py
por el nombre del directorio y el archivo
en el que copió el código.
!python ../scripts/mypy_example.py
python: can't open file '/home/fiol/Clases/IntPython/clases-python/clases/../scripts/mypy_example.py': [Errno 2] No such file or directory
Ahora hagamos lo mismo con mypy
:
!mypy ../scripts/mypy_example.py
mypy: can't read file '../scripts/mypy_example.py': No such file or directory
MyPy
y notebooks
En el caso de los notebooks de Python el manejo de tipos estáticos puede
ser más complejo, dado que de alguna forma, la aplicación mypy
tiene
que correr simultaneamente (o en segundo plano) con el intérprete
interactivo. La mejor opción es el módulo
`nb_mypy
<https://gitlab.tue.nl/jupyter-projects/nb_mypy>`__ que
podemos instalar con:
pip install nb_mypy
Luego cargamos la extensión a nuestro notebook con el comando mágico
%load_ext
:
%load_ext nb_mypy
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[4], line 1
----> 1 get_ipython().run_line_magic('load_ext', 'nb_mypy')
File /usr/lib/python3.13/site-packages/IPython/core/interactiveshell.py:2480, in InteractiveShell.run_line_magic(self, magic_name, line, _stack_depth)
2478 kwargs['local_ns'] = self.get_local_scope(stack_depth)
2479 with self.builtin_trap:
-> 2480 result = fn(*args, **kwargs)
2482 # The code below prevents the output from being displayed
2483 # when using magics with decorator @output_can_be_silenced
2484 # when the last Python token in the expression is a ';'.
2485 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):
File /usr/lib/python3.13/site-packages/IPython/core/magics/extension.py:33, in ExtensionMagics.load_ext(self, module_str)
31 if not module_str:
32 raise UsageError('Missing module name.')
---> 33 res = self.shell.extension_manager.load_extension(module_str)
35 if res == 'already loaded':
36 print("The %s extension is already loaded. To reload it, use:" % module_str)
File /usr/lib/python3.13/site-packages/IPython/core/extensions.py:62, in ExtensionManager.load_extension(self, module_str)
55 """Load an IPython extension by its module name.
56
57 Returns the string "already loaded" if the extension is already loaded,
58 "no load function" if the module doesn't have a load_ipython_extension
59 function, or None if it succeeded.
60 """
61 try:
---> 62 return self._load_extension(module_str)
63 except ModuleNotFoundError:
64 if module_str in BUILTINS_EXTS:
File /usr/lib/python3.13/site-packages/IPython/core/extensions.py:77, in ExtensionManager._load_extension(self, module_str)
75 with self.shell.builtin_trap:
76 if module_str not in sys.modules:
---> 77 mod = import_module(module_str)
78 mod = sys.modules[module_str]
79 if self._call_load_ipython_extension(mod):
File /usr/lib64/python3.13/importlib/__init__.py:88, in import_module(name, package)
86 break
87 level += 1
---> 88 return _bootstrap._gcd_import(name[level:], package, level)
File <frozen importlib._bootstrap>:1387, in _gcd_import(name, package, level)
File <frozen importlib._bootstrap>:1360, in _find_and_load(name, import_)
File <frozen importlib._bootstrap>:1324, in _find_and_load_unlocked(name, import_)
ModuleNotFoundError: No module named 'nb_mypy'
De esta forma, de ahora en más mypy estará chequeando los tipos de datos anotados:
def cube(x: int) -> int:
return x*x*x
def Hola(name: str) -> str:
return 'Hola ' + name
def Hola2(name):
return 'Hola ' + name
a = cube(2)
print(f"El cubo de 2 es: {a}")
b = cube(3.0) # Esto no da error en Python, mypy si lo captura
print(f"El cubo de 3.0 es: {b}")
print(f"Hola('Juan'): {Hola('Juan')}")
print(f"Hola2(3): {Hola2(3)}") # Esto da un error de concatenación
print(f"Hola(3): {Hola(3)}") # Esto da un error de concatenación, y además mypy lo captura
El cubo de 2 es: 8
El cubo de 3.0 es: 27.0
Hola('Juan'): Hola Juan
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 10
7 print(f"El cubo de 3.0 es: {b}")
9 print(f"Hola('Juan'): {Hola('Juan')}")
---> 10 print(f"Hola2(3): {Hola2(3)}") # Esto da un error de concatenación
12 print(f"Hola(3): {Hola(3)}") # Esto da un error de concatenación, y además mypy lo captura
Cell In[5], line 8, in Hola2(name)
7 def Hola2(name):
----> 8 return 'Hola ' + name
TypeError: can only concatenate str (not "int") to str
Podemos evitar el control de tipos con:
%nb_mypy Off
UsageError: Line magic function %nb_mypy not found.
a = cube(2)
print(f"El cubo de 2 es: {a}")
b = cube(3.0) # Esto no da error en Python, mypy si lo captura
print(f"El cubo de 3.0 es: {b}")
print(f"Hola('Juan'): {Hola('Juan')}")
print(f"Hola2(3): {Hola2(3)}") # Esto da un error de concatenación
print(f"Hola(3): {Hola(3)}") # Esto da un error de concatenación, y además mypy lo captura
El cubo de 2 es: 8
El cubo de 3.0 es: 27.0
Hola('Juan'): Hola Juan
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 10
7 print(f"El cubo de 3.0 es: {b}")
9 print(f"Hola('Juan'): {Hola('Juan')}")
---> 10 print(f"Hola2(3): {Hola2(3)}") # Esto da un error de concatenación
12 print(f"Hola(3): {Hola(3)}") # Esto da un error de concatenación, y además mypy lo captura
Cell In[5], line 8, in Hola2(name)
7 def Hola2(name):
----> 8 return 'Hola ' + name
TypeError: can only concatenate str (not "int") to str
El módulo ``nb_mypy`` puede utilizarse en cualquier editor de
notebooks, ya sea Jupyter, Jupyterlabs, VS Code, etc.
Opciones en VSCode
Para los usuarios de VS Code, existen dos extensiones que permiten
utilizar mypy
:
MyPy Type Checker, que muestra los errores directamente en el código, pero no funciona todavía con notebooks
Mypy, que funciona igual que la anterior, y tiene, en forma experimental la opción de comprobar tipos en notebooks:
Ir a Settings, y buscar
mypy.checkNotebooks
, y setearlo atrue
.