Link Search Menu Expand Document

Anotaciones en Funciones

Function Annotations en Python

Las anotaciones en funciones o function annotations de Python nos permiten a帽adir el tipo de los argumentos de entrada y salida de una funci贸n. A continuaci贸n podemos ver un ejemplo con la funci贸n suma(), que recibe dos argumentos a, b y cuyo tipo se espera que sea int.

def suma(a: int, b: int) -> int:
    return a + b

print(suma(7, 3))
#聽Salida: 10

Sin embargo es muy importante notar que Python ignora las anotaciones. Es decir, son una mera nota en el c贸digo que indica el tipo esperado, pero el siguiente c贸digo no dar铆a ning煤n error. M谩s adelante explicaremos c贸mo realizar el chequeo de tipos.

suma(7.0, 3.0)

Las anotaciones en funciones fueron introducidas en la PEP3107 para Python 3, y m谩s adelante se introdujo la PEP484 especificando la sem谩ntica que se debe usar.

Motivaci贸n y Necesidad de las Anotaciones

Python es un lenguaje de programaci贸n con tipado din谩mico y duck typing, lo que significa que los tipos (int, string, etc) le dan igual. Precisamente esto es lo que hace que el siguiente c贸digo funcione. La funci贸n imprime puede ser llamada con cualquier tipo, ya que Python no realiza ninguna comprobaci贸n del tipo de var.

def imprime(var):
    print(var)

imprime(1.0)      # 1.0
imprime(3)        # 3
imprime("Python") # Python

Sin embargo, en ciertas ocasiones esto nos puede traer problemas. 驴Y si queremos que la funci贸n imprime s贸lo acepte que var sea de un tipo concreto? Pues bien, las anotaciones en funciones o function annotations como acabamos de ver nos permiten especificar los tipos que se esperan recibir.

Ejemplos de Function Annotations

Antes de nada es importante notar que las anotaciones en funciones no definen per se una sem谩ntica propia. Es decir, podemos escribir lo que se nos ocurra despu茅s de cada argumento. Las anotaciones pueden ser accedidas usando __annotations__.

def suma(a: 'parametro 1', b: 'parametro 2') -> 'retorno':
    return a + b

print(suma.__annotations__)
# Salida:
# {'a': 'parametro 1',
#  'b': 'parametro 2',
#  'return': 'retorno'}

Aunque como hemos dicho se puedan realizar anotaciones arbitrarias, suele ser com煤n usar tipos de Python como int, str o float. En el siguiente ejemplo podemos ver como se combina una anotaci贸n con un valor por defecto [].

def filtrar_pares(salida: 'list' = []) -> 'list':
    return [i for i in salida if i%2 == 0]

print(filtrar_pares([1, 2, 3, 4, 5, 6]))
# Salida: [2, 4, 6]

Tambi茅n es posible usar como anotaciones clases definidas por nosotros, como ClaseA.

class ClaseA:
    pass

def funcion(a: ClaseA) -> ClaseA:
    return a

a = ClaseA()
funcion(a)

Por 煤ltimo, las anotaciones no est谩n limitadas a los argumentos de las funciones, sino que tambi茅n se pueden asignar a variables de declaremos.

pi: float = 3.14

print(pi)
# Salida: 3.14

Sin embargo, como ya hemos visto anteriormente, Python no da error cuando los tipos no se corresponden como en el ejemplo anterior.

# Ojo: No ser铆a correcto, pero Python no da error
pi: int = 3.14

print(pi)
# Salida: 3.14

Entonces uno se puede preguntar, 驴y para que sirven las function annotation, si Python las ignora? Pues bien, aunque las ignore, tenemos herramientas que nos permiten saber cuando no se est谩n respetando. Lo vemos a continuaci贸n con el an谩lisis est谩tico del c贸digo.

Uso de mypy y Static Type Checking

Una primera forma de verificar que las funciones se llaman con los par谩metros especificados por las anotaciones, ser铆a lo siguiente. Sin embargo el error que obtendr铆amos ser铆a en tiempo de ejecuci贸n. Es decir, nos encontrar铆amos con el error una vez el c贸digo estuviera ejecut谩ndose. Por lo tanto, no recomendamos el uso del siguiente c贸digo.

# Nota: Ejemplo did谩ctico, no recomendado
def suma(a: int, b: int) -> int:
    if isinstance(a, suma.__annotations__['a']) and isinstance(b, suma.__annotations__['b']):
        return a + b
    else:
        raise Exception("Error de tipos")

print(suma(7, 3))
# Salida: 10

print(suma(7.0, 3.0))
# Salida: Exception: Error de tipos

Afortunadamente, tenemos herramientas como mypy que nos permiten hacer un chequeo est谩tico de los tipos, obteniendo el error antes de que el c贸digo se ejecute. Lo podemos instalar de la siguiente manera.

$ pip instal mypy

Y volviendo al ejemplo anterior de la suma, podemos ver como el siguiente c贸digo si que pasar铆a los checks de mypy.

# suma_correcta.py
def suma(a: int, b: int) -> int:
    return a + b

print(suma(7, 3))
$ mypy suma_correcta.py 
Success: no issues found in 1 source file

Sin embargo si cambiamos los tipos de los par谩metros de entrada, obtendremos un error.

# suma_incorrecta.py
def suma(a: int, b: int) -> int:
    return a + b

print(suma(7.0, "3"))
$ mypy suma_incorrecta.py
suma_incorrecta.py:5: error: Argument 1 to "suma" has incompatible type "float"; expected "int"
suma_incorrecta.py:5: error: Argument 2 to "suma" has incompatible type "str"; expected "int"
Found 2 errors in 1 file (checked 1 source file)

Como hemos indicado, la ventaja de mypy es que realiza un an谩lisis est谩tico, es decir, sin ejecutar el c贸digo. Esto es algo muy importante ya que si de verdad queremos reforzar que se verifiquen los tipos, no tendr铆a mucho sentido hacerlo en tiempo de ejecuci贸n, ya que ser铆a demasiado tarde y tendr铆amos un error.