Módulo de escritura en Python: Utiliza los verificadores de tipo de manera efectiva

Desde Python 3.5 en adelante, el módulo de typing de Python intenta proporcionar una forma de dar pistas sobre los tipos para ayudar a los verificadores de tipos estáticos y a los linters a predecir errores con precisión.

Debido a que Python tiene que determinar el tipo de objetos durante el tiempo de ejecución, a veces resulta muy difícil para los desarrolladores entender qué está sucediendo exactamente en el código.

Incluso los verificadores de tipos externos como PyCharm IDE no producen los mejores resultados; en promedio, solo predicen correctamente los errores alrededor del 50% del tiempo, según esta respuesta en StackOverflow.

Python intenta mitigar este problema introduciendo lo que se conoce como type hinting (anotación de tipo) para ayudar a que los verificadores de tipos externos identifiquen cualquier error. Esta es una buena manera para que el programador dé pistas sobre el tipo de objeto(s) que se están utilizando, durante el tiempo de compilación, y asegurarse de que los verificadores de tipos funcionen correctamente.

¡Esto hace que el código de Python sea mucho más legible y robusto para otros lectores también!

NOTA: Esto no realiza verificación real de tipos en tiempo de compilación. Si el objeto real devuelto no era del mismo tipo que se indicó, no habrá ningún error de compilación. Por eso utilizamos verificadores de tipos externos, como mypy, para identificar cualquier error de tipo.


Para utilizar el módulo typing de manera efectiva, se recomienda que utilice un comprobador/linter de tipos externo para verificar la coincidencia de tipos estáticos. Uno de los comprobadores de tipos más ampliamente utilizados en Python es mypy, así que recomiendo que lo instale antes de leer el resto del artículo.

Ya hemos cubierto los conceptos básicos de comprobación de tipos en Python. Puede revisar este artículo primero.

Utilizaremos mypy como el comprobador de tipos estáticos en este artículo, que se puede instalar mediante:

pip3 install mypy

Puede ejecutar mypy en cualquier archivo de Python para verificar si los tipos coinciden. Esto es como si estuviera ‘compilando’ código Python.

mypy program.py

Después de depurar errores, puede ejecutar el programa normalmente utilizando:

python program.py

Ahora que hemos cubierto nuestros prerrequisitos, intentemos utilizar algunas de las características del módulo.


Anotaciones de tipos

En funciones

Podemos anotar una función para especificar su tipo de retorno y los tipos de sus parámetros.

def print_list(a: list) -> None:
    print(a)

Esto informa al comprobador de tipos (mypy en mi caso) que tenemos una función print_list() que tomará una lista como argumento y devolverá None.

def print_list(a: list) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

Vamos a ejecutar esto primero en nuestro comprobador de tipos mypy:

vijay@JournalDev:~ $ mypy printlist.py 
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)

Como era de esperar, obtenemos un error; ya que la línea #5 tiene el argumento como un int, en lugar de una lista.

En variables

Desde Python 3.6, también podemos anotar los tipos de variables, mencionando el tipo. Pero esto no es obligatorio si deseas que el tipo de una variable cambie antes de que la función devuelva un valor.

# Anota 'radio' como un float
radius: float = 1.5

# Podemos anotar una variable sin asignarle un valor
sample: int

# Anota 'área' para devolver un float
def area(r: float) -> float:
    return 3.1415 * r * r


print(area(radius))

# Imprime todas las anotaciones de la función usando
# el diccionario '__annotations__'
print('Dictionary of Annotations for area():', area.__annotations__)

Salida de mypy:

vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}

Este es el modo recomendado de utilizar mypy, proporcionando primero anotaciones de tipo antes de ejecutar el verificador de tipos.


Alias de Tipos

El módulo typing nos proporciona Alias de Tipos, que se define asignando un tipo al alias.

from typing import List

# Vector es una lista de valores flotantes
Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)

Salida

vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]

En el fragmento anterior, Vector es un alias que representa una lista de valores de punto flotante. Podemos hacer una sugerencia de tipo en un alias, que es lo que hace el programa anterior.

La lista completa de alias aceptables se encuentra aquí.

Vamos a ver otro ejemplo, que verifica cada par clave:valor en un diccionario y verifica si coinciden con el formato nombre:correo electrónico.

from typing import Dict
import re

# Crea un alias llamado 'ContactDict'
ContactDict = Dict[str, str]

def check_if_valid(contacts: ContactDict) -> bool:
    for name, email in contacts.items():
        # Verifica si nombre y correo electrónico son cadenas
        if (not isinstance(name, str)) or (not isinstance(email, str)):
            return False
        # Verifica el formato de correo electrónico [email protected]
        if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
            return False
    return True


print(check_if_valid({'vijay': '[email protected]'}))
print(check_if_valid({'vijay': '[email protected]', 123: '[email protected]'}))

Salida de mypy

vijay@JournalDev:~ $ mypy validcontacts.py 
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)

Aquí, obtenemos un error estático en tiempo de compilación en mypy, ya que el parámetro nombre en nuestro segundo diccionario es un entero (123). Así, los alias son otra forma de garantizar una verificación precisa del tipo por parte de mypy.


Crear tipos de datos definidos por el usuario utilizando NewType()

Podemos utilizar la función NewType() para crear nuevos tipos definidos por el usuario.

from typing import NewType

# Crear un nuevo tipo de usuario llamado 'StudentID' que consiste en
# un entero
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)

El verificador de tipo estático tratará el nuevo tipo como si fuera una subclase del tipo original. Esto es útil para detectar errores lógicos.

from typing import NewType

# Crear un nuevo tipo de usuario llamado 'StudentID'
StudentID = NewType('StudentID', int)

def get_student_name(stud_id: StudentID) -> str:
    return str(input(f'Enter username for ID #{stud_id}:\n'))

stud_a = get_student_name(StudentID(100))
print(stud_a)

# ¡Esto es incorrecto!
stud_b = get_student_name(-1)
print(stud_b)

Salida de mypy

vijay@JournalDev:~ $ mypy studentnames.py  
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)

El tipo Any

Este es un tipo especial, informando al verificador de tipo estático (mypy en mi caso) que cada tipo es compatible con esta palabra clave.

Ten en cuenta nuestra antigua función print_list(), que ahora acepta argumentos de cualquier tipo.

from typing import Any

def print_list(a: Any) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

Ahora, no habrá errores cuando ejecutemos mypy.

vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1

Todas las funciones sin un tipo de retorno o tipos de parámetros utilizarán implícitamente Any.

def foo(bar):
    return bar

# Un verificador de tipo estático tratará lo anterior
# como si tuviera la misma firma que:
def foo(bar: Any) -> Any:
    return bar

Por lo tanto, puedes usar Any para mezclar código tipado estática y dinámicamente.


Conclusión

En este artículo, hemos aprendido sobre el módulo de typing de Python, que es muy útil en el contexto de la comprobación de tipos, permitiendo a verificadores de tipos externos como mypy informar con precisión sobre cualquier error.

Esto nos proporciona una forma de escribir código tipado estáticamente en Python, ¡que es un lenguaje tipado dinámicamente por diseño!


Referencias


Source:
https://www.digitalocean.com/community/tutorials/python-typing-module