Python 3 Sugerencias de tipo y análisis estático
Spanish (Español) translation by Juan Pablo Diaz Cuartas (you can also view the original English article)
Python 3.5 introdujo el nuevo módulo de tipado que proporciona soporte de biblioteca estándar para aprovechar las anotaciones de funciones para sugerencias de tipos opcionales. Esto abre la puerta a herramientas nuevas e interesantes para la comprobación de tipos estáticos como mypy y en el futuro, posiblemente, la optimización automática basada en tipos. Las sugerencias de tipo se especifican en PEP-483 y PEP-484.
En este tutorial explorar las posibilidades que escriba consejos presentes y mostrarle cómo utilizar mypy para analizar estadísticamente los programas Python y mejorar significativamente la calidad de su código.
Tipo Sugerencias
Las sugerencias de tipo se crean encima de las anotaciones de funciones. Brevemente, las anotaciones de función permiten anotar los argumentos y devuelve el valor de una función o método con metadatos arbitrario. Tipo de sugerencias es un caso especial de anotaciones de función que específicamente anotar argumentos de la función y el valor devuelto con información de tipo estándar. Anotaciones de la función en general y sugerencias de tipo en particular son totalmente opcionales. Veamos un ejemplo rápido:
1 |
def reverse_slice(text: str, start: int, end: int) -> str: |
2 |
return text[start:end][::-1] |
3 |
|
4 |
reverse_slice('abcdef', 3, 5) |
5 |
'ed'
|
Los argumentos fueron anotados con su tipo, así como el valor devuelto. Sin embargo, es importante darse cuenta que Python ignora esto completamente. Hace que la información de tipo disponibles a través del atributo de anotaciones del objeto función, pero que está sobre él.
1 |
reverse_slice.__annotations |
2 |
{'end': int, 'return': str, 'start': int, 'text': str} |
Para verificar que Python ignora realmente los consejos del tipo, vamos a totalmente desordenar las sugerencias del tipo:
1 |
def reverse_slice(text: float, start: str, end: bool) -> dict: |
2 |
return text[start:end][::-1] |
3 |
|
4 |
reverse_slice('abcdef', 3, 5) |
5 |
'ed'
|
Como se puede ver, el código comporta de la misma, sin tener en cuenta las sugerencias de tipo.
Motivación para indicaciones de tipo
Vale. Tipo sugerencias son opcionales. Tipo consejos son ignorados totalmente por Python. ¿Cuál es el punto de ellos, entonces? Bueno, hay varias buenas razones:
- Análisis estático
- Apoyo IDE
- documentación estándar
A buceo en análisis estático con Mypy más tarde. Apoyo IDE ya iniciado con apoyo de PyCharm 5 para sugerencias de tipo. Documentación estándar es ideal para desarrolladores que pueden fácilmente averiguar el tipo de argumentos y valor devuelto solo con mirar a una firma de la función como generadores de documentación automatizado que pueden extraer la información del tipo de las pistas.
El módulo de mecanografía
El módulo de mecanografía contiene tipos diseñados para admitir sugerencias de tipo. ¿Por qué no usar los tipos de Python como int, str, lista y dict? Definitivamente puede utilizar estos tipos, pero debido a la tipificación dinámica de Python, más allá de los tipos básicos no te mucha información. Por ejemplo, si desea especificar que un argumento puede ser una asignación entre una cadena y un entero, no hay ninguna manera de hacerlo con los tipos estándar de Python. Con el módulo de mecanografía, es tan fácil como:
1 |
Mapping[str, int] |
Veamos un ejemplo más completo: una función que toma dos argumentos. Uno de ellos es una lista de diccionarios donde cada diccionario contiene claves que son cadenas y valores que son números enteros. El otro argumento es una cadena o un entero. El módulo de mecanografía permite especificaciones precisas de estos argumentos complicados.
1 |
from typing import List, Dict, Union |
2 |
|
3 |
def foo(a: List[Dict[str, int]], |
4 |
b: Union[str, int]) -> int: |
5 |
"""Print a list of dictionaries and return the number of dictionaries
|
6 |
"""
|
7 |
if isinstance(b, str): |
8 |
b = int(b) |
9 |
for i in range(b): |
10 |
print(a) |
11 |
|
12 |
|
13 |
x = [dict(a=1, b=2), dict(c=3, d=4)] |
14 |
foo(x, '3') |
15 |
|
16 |
[{'b': 2, 'a': 1}, {'d': 4, 'c': 3}] |
17 |
[{'b': 2, 'a': 1}, {'d': 4, 'c': 3}] |
18 |
[{'b': 2, 'a': 1}, {'d': 4, 'c': 3}] |
Tipos de útiles
Vamos a ver algunos de los tipos más interesantes desde el módulo de mecanografía.
El tipo puede llamar le permite especificar la función que puede ser pasada como argumentos o devueltos como resultado, puesto que Python trata funciones como ciudadanos de primera clase. La sintaxis para amortizables es proporcionar una matriz de tipos de argumentos (otra vez desde el módulo de mecanografía) seguida por un valor de retorno. Si eso es confuso, aquí está un ejemplo:
1 |
def do_something_fancy(data: Set[float], on_error: Callable[[Exception, int], None]): |
2 |
...
|
3 |
La función de devolución de llamada on_error se especifica como una función que toma una excepción y un entero como argumento y devuelve nothing.
Cualquier tipo significa que un comprobador de tipo estático debe permitir que cualquier tipo de operación así como la cesión a cualquier otro tipo. Cada tipo es un subtipo de cualquiera.
El tipo de Unión que vimos anteriormente es útil cuando un argumento puede tener múltiples tipos, que es muy común en Python. En el ejemplo siguiente, la función verify_config() acepta un argumento de configuración, que puede ser un objeto de configuración o un nombre de archivo. Si es un nombre de archivo, llama a otra función para analizar el archivo en un objeto de configuración y volver.
1 |
def verify_config(config: Union[str, Config]): |
2 |
if isinstance(config, str): |
3 |
config = parse_config_file(config) |
4 |
...
|
5 |
|
6 |
def parse_config_file(filename: str) -> Config: |
7 |
...
|
8 |
El tipo opcional significa que el argumento no puede ser demasiado. Opcional [T] es equivalente a la Unión [T, ninguno]
Hay muchos más tipos que denotan diferentes capacidades como Iterable, iterador, Reversible, SupportsInt, SupportsFloat, secuencia, MutableSequence y IO. Revisa la documentación del módulo mecanografía la lista completa.
Lo principal es que puede especificar el tipo de argumentos de una manera muy de grano fino que apoya el sistema de tipo de Python en una alta fidelidad y permite demasiado genéricos y clases base abstractas.
Referencias hacia adelantados
A veces desea hacer referencia a una clase en una pista tipo dentro de uno de sus métodos. Por ejemplo, supongamos que la clase A puede realizar alguna operación de combinación que toma otra instancia de la A, se combina con sí mismo y devuelve el resultado. Aquí es un ingenuo intento usar insinuaciones de tipo especificarlo:
1 |
class A: |
2 |
def merge(other: A) -> A: |
3 |
...
|
4 |
|
5 |
1 class A: |
6 |
----> 2 def merge(other: A = None) -> A: |
7 |
3 ... |
8 |
4
|
9 |
|
10 |
NameError: name 'A' is not defined |
¿Qué ha pasado? La clase A no está definido aún cuando la sugerencia de tipo para su método merge() por Python, por lo que la clase A no se puede utilizar en este punto (directamente). La solución es bastante sencilla, y lo he visto usado antes por SQLAlchemy. Simplemente especifique la sugerencia del tipo como una cadena. Python se entiende que es una referencia adelantada y va a hacer lo correcto:
1 |
class A: |
2 |
def merge(other: 'A' = None) -> 'A': |
3 |
...
|
Alias de tipo
Una desventaja de usar tipo consejos para larga tipo especificaciones es que puede desorden el código y hacer menos legible, incluso si proporciona mucha información de tipo. Puede tipos de alias al igual que cualquier otro objeto. Es tan simple como:
1 |
Data = Dict[int, Sequence[Dict[str, Optional[List[float]]]] |
2 |
|
3 |
def foo(data: Data) -> bool: |
4 |
...
|
Get_type_hints() función auxiliar
El módulo de escritura proporciona la función get_type_hints(), que proporciona información sobre los tipos de argumento y el valor de retorno. Mientras que el atributo de anotaciones devuelve tipo de consejos porque son sólo anotaciones, todavía recomienda que utilice la función get_type_hints() porque resuelve referencias hacia delanteras. Además, si especifica un valor predeterminado de ninguno a uno de los argumentos, la función de get_type_hints() volverá automáticamente su tipo como Unión [T, NoneType] si sólo especificado T. Vamos a ver la diferencia que el método A.merge () definida anteriormente:
1 |
print(A.merge.__annotations__) |
2 |
|
3 |
{'other': 'A', 'return': 'A'} |
El atributo de anotaciones simplemente devuelve el valor de la anotación como es. En este caso es sólo la cadena 'A' y no el un clase objeto, para que 'A' es una referencia hacia delante.
1 |
print(get_type_hints(A.merge)) |
2 |
|
3 |
{'return': <class '__main__.A'>, 'other': typing.Union[__main__.A, NoneType]} |
La función get_type_hints() convierte al tipo de la otra discusión a una Unión de (la clase) y NoneType debido al ninguno por defecto argumento. El tipo de valor devuelto se convierte también en la clase A.
Los decoradores
Tipo sugerencias son una especialización de las anotaciones de la función, y también pueden trabajar codo a codo con otra anotación de la función.
Para ello, el módulo de escritura proporciona dos decoradores: @no_type_check y @no_type_check_decorator. El decorador de @no_type_check puede aplicarse a una clase o una función. Agrega el atributo no_type_check a la función (o cada método de la clase). Así, tipo damas saber ignorar las anotaciones, que no son consejos de tipo.
Es un poco engorroso porque si escribes una biblioteca que se utilizará ampliamente, debe asumir que un tipo a utilizar, y si quieres anotar tus funciones con toques sin tipo, también debe decorarlos con @no_type_check.
Un escenario común al usar las anotaciones de la función normal es también que un decorador que opera sobre ellos. También desea apagar el tipo de control en este caso. Una opción es utilizar el decorador de @no_type_check además de su decorador, pero que envejece. En cambio, el @no_Type_check_decorator puede utilizarse para decorar su decorador para que también se comporta como @no_type_check (agrega el atributo no_type_check).
Permítanme ilustrar estos conceptos. Si tratas de get_type_hint() (como hará el corrector de cualquier tipo) en una función que se anota con una anotación de cadena regular, el get_type_hints() la interpretará como una referencia hacia delante:
1 |
def f(a: 'some annotation'): |
2 |
pass
|
3 |
|
4 |
print(get_type_hints(f)) |
5 |
|
6 |
SyntaxError: ForwardRef must be an expression -- got 'some annotation' |
Para evitarlo, agregue el decorador de @no_type_check y get_type_hints simplemente devuelve un dict vacía, mientras que el atributo de __annotations__ devuelve las anotaciones:
1 |
@no_type_check |
2 |
def f(a: 'some annotation'): |
3 |
pass
|
4 |
|
5 |
print(get_type_hints(f)) |
6 |
{}
|
7 |
|
8 |
print(f.__annotations__) |
9 |
{'a': 'some annotation'} |
Ahora, supongamos que tenemos un decorador que imprime la dict. de anotaciones Puede decorar con el @no_Type_check_decorator y luego decorar la función y no preocuparse de algunos checker tipo llamando a get_type_hints() y conseguir confundido. Esto es probablemente una mejor práctica para cada decorador que opera en las anotaciones. No se olvide de la @functools.wraps, caso contrario que las anotaciones no se copiarán a la función de decoración y todo se deshagan. Esto se cubre en detalle en Python 3 función de anotaciones.
1 |
@no_type_check_decorator |
2 |
def print_annotations(f): |
3 |
@functools.wraps(f) |
4 |
def decorated(*args, **kwargs): |
5 |
print(f.__annotations__) |
6 |
return f(*args, **kwargs) |
7 |
return decorated |
Ahora puedes decorar la función sólo con @print_annotations, y cada vez que se llama imprimirá sus anotaciones.
1 |
@print_annotations |
2 |
def f(a: 'some annotation'): |
3 |
pass
|
4 |
|
5 |
f(4) |
6 |
{'a': 'some annotation'} |
Llamar get_type_hints() es también segura y devuelve un vacío dict.
1 |
print(get_type_hints(f)) |
2 |
{}
|
Análisis estático con Mypy
Mypy es un comprobador de tipo estático que fue la inspiración para tipo consejos y el módulo de mecanografía. Guido van Rossum a sí mismo es el autor de PEP-483 y coautor de PEP-484.
Instalar Mypy
Mypy está en desarrollo muy activo, y a partir de esta escritura el paquete en PyPI es obsoleto y no funciona con Python 3.5. Para usar Mypy con Python 3.5, haz la última desde el repositorio de Mypy en GitHub. Es tan simple como:
1 |
pip3 install git+git://github.com/JukkaL/mypy.git
|
Jugando con Mypy
Una vez que Mypy instalado, sólo puede ejecutar Mypy en sus programas. El siguiente programa define una función que espera una lista de cadenas. Luego invoca la función con una lista de enteros.
1 |
from typing import List |
2 |
|
3 |
def case_insensitive_dedupe(data: List[str]): |
4 |
"""Converts all values to lowercase and removes duplicates"""
|
5 |
return list(set(x.lower() for x in data)) |
6 |
|
7 |
|
8 |
print(case_insensitive_dedupe([1, 2])) |
Cuando se ejecuta el programa, obviamente no en tiempo de ejecución con el siguiente error:
1 |
python3 dedupe.py |
2 |
Traceback (most recent call last): |
3 |
File "dedupe.py", line 8, in <module> |
4 |
print(case_insensitive_dedupe([1, 2, 3])) |
5 |
File "dedupe.py", line 5, in case_insensitive_dedupe |
6 |
return list(set(x.lower() for x in data)) |
7 |
File "dedupe.py", line 5, in <genexpr> |
8 |
return list(set(x.lower() for x in data)) |
9 |
AttributeError: 'int' object has no attribute 'lower' |
¿Cuál es el problema con eso? El problema es que no es inmediatamente claro incluso en este caso muy simple cuál es la causa. ¿Es un problema de tipo de entrada? O tal vez el código sí mismo es malo y no debe intentar llamar al método lower() en el objeto 'int'. Otra cuestión es que si no tienes 100% de cobertura de pruebas (y, seamos sinceros, que ninguno de nosotros), entonces esas cuestiones pueden esconderse en algún camino de código no probados, raramente usado y detectadas en el peor momento en la producción.
Escribir estática, ayudado por consejos del tipo, da una red de seguridad extra haciendo lo siempre llamar sus funciones (anotados con notas de tipo) con los tipos de derecho. Aquí está la salida de Mypy:
1 |
(N) > mypy dedupe.py |
2 |
dedupe.py:8: error: List item 0 has incompatible type "int" |
3 |
dedupe.py:8: error: List item 1 has incompatible type "int" |
4 |
dedupe.py:8: error: List item 2 has incompatible type "int" |
Esto es sencillo, apunta directamente al problema y no requiere correr un montón de pruebas. Otro beneficio de tipo estático comprobar es que si te comprometes a él, puede omitir la comprobación excepto cuando la entrada externa (lectura de archivos, las peticiones de red o usuario) de análisis de tipo dinámico. También crea mucha confianza en lo va de refactorización.
Conclusión
Tipo sugerencias y el módulo de escritura son totalmente opcionales adiciones a la expresividad de Python. Mientras que pueden no satisfacer todos los gustos, para grandes proyectos y grandes equipos pueden ser indispensables. La evidencia es que equipos grandes ya utilizan comprobación de tipo estático. Ahora que se ha estandarizado la información de tipo, será más fácil compartir el código, utilidades y herramientas que utilizan. IDEs como PyCharm ya aprovechar para proporcionar una mejor experiencia de desarrollador.



