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)

  1. Defina una función que dada una altura inicial h_0, una velocidad inicial v_0 y un paso de tiempo delta_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

  2. 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 nombre x 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)

  1. Escriba funciones para analizar la divisibilidad de enteros:

    • La función es_divisible que retorna True si el argumento x es divisible por alguno de los elemntos del segundo argumento: divisores. El argumento divisores 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) es x, 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
      
  2. 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

    1. 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"""
    
    1. 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.

    2. Escribir la función check_sudoku() que toma como argumento una grilla (como una lista bidimensional de 9x9) 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 de 3x3. 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, cuando i toma valores entre 0 y 8.

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 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 a Union[<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 a true.