Introducción a las anotaciones de tipo Python

Introduccion



Ilustración de Magdalena Tomczyk.


Segunda parte


Python es un lenguaje con tipeo dinámico y nos permite manipular libremente variables de diferentes tipos. Sin embargo, al escribir código, de una forma u otra asumimos qué tipos de variables se usarán (esto puede ser causado por una restricción del algoritmo o la lógica empresarial). Y para el correcto funcionamiento del programa, es importante que encontremos los errores lo antes posible asociados con la transferencia de datos del tipo incorrecto.


Manteniendo la idea del tipeo dinámico de pato en las versiones modernas de Python (3.6+), admite anotaciones de tipos variables, campos de clase, argumentos y valores de retorno de funciones:



Las anotaciones de tipo son simplemente leídas por el intérprete de Python y ya no se procesan, sino que están disponibles para su uso a partir de código de terceros y están diseñadas principalmente para su uso por analizadores estáticos.


Mi nombre es Andrey Tikhonov y estoy involucrado en el desarrollo de backend en Lamoda.


En este artículo, quiero explicar los conceptos básicos del uso de anotaciones de tipo y considerar los ejemplos típicos implementados al typing anotaciones.


Herramientas de anotación


Las anotaciones de tipo son compatibles con muchos IDE de Python que resaltan el código incorrecto o proporcionan sugerencias mientras escribe.


Por ejemplo, así es como se ve en Pycharm:


Error al resaltar



Consejos:



Las anotaciones de tipo también son procesadas por linters de consola.


Aquí está la salida de pylint:


 $ pylint example.py ************* Module example example.py:7:6: E1101: Instance of 'int' has no 'startswith' member (no-member) 

Pero para el mismo archivo que encontró mypy:


 $ mypy example.py example.py:7: error: "int" has no attribute "startswith" example.py:10: error: Unsupported operand types for // ("str" and "int") 

El comportamiento de diferentes analizadores puede variar. Por ejemplo, mypy y pycharm manejan cambiar el tipo de una variable de manera diferente. Además en los ejemplos, me centraré en la salida de mypy.


En algunos ejemplos, el código puede ejecutarse sin excepción al inicio, pero puede contener errores lógicos debido al uso de variables del tipo incorrecto. Y en algunos ejemplos, puede que ni siquiera se ejecute.


Los fundamentos


A diferencia de las versiones anteriores de Python, las anotaciones de tipo no están escritas en comentarios o cadenas de documentos, sino directamente en el código. Por un lado, esto rompe la compatibilidad hacia abajo, por otro lado, claramente significa que es parte del código y puede procesarse en consecuencia


En el caso más simple, la anotación contiene el tipo directamente esperado. Casos más complejos se discutirán a continuación. Si la clase base se especifica como una anotación, es aceptable pasar instancias de sus descendientes como valores. Sin embargo, solo puede usar las características que se implementan en la clase base.


Las anotaciones para las variables se escriben después de los dos puntos después del identificador. Después de esto, el valor puede ser inicializado. Por ejemplo


 price: int = 5 title: str 

Los parámetros de función se anotan de la misma manera que las variables, y el valor de retorno se indica después de la flecha -> y antes de los dos puntos finales. Por ejemplo


 def indent_right(s: str, width: int) -> str: return " " * (max(0, width - len(s))) + s 

Para los campos de clase, las anotaciones deben especificarse explícitamente al definir una clase. Sin embargo, los analizadores pueden __init__ automáticamente en función del método __init__ , pero en este caso no estarán disponibles en tiempo de ejecución. Lea más sobre cómo trabajar con anotaciones en tiempo de ejecución en la segunda parte del artículo


 class Book: title: str author: str def __init__(self, title: str, author: str) -> None: self.title = title self.author = author b: Book = Book(title='Fahrenheit 451', author='Bradbury') 

Por cierto, cuando se usa la clase de datos, los tipos de campo deben especificarse en la clase. Más acerca de la clase de datos


Tipos incorporados


Aunque puede usar tipos estándar como anotaciones, muchas cosas útiles están ocultas en el módulo de typing .


Opcional


Si marca la variable con el tipo int e intenta asignarle None , habrá un error:


Incompatible types in assignment (expression has type "None", variable has type "int")


Para tales casos, se proporciona una anotación Optional con un tipo específico en el módulo de escritura. Tenga en cuenta que el tipo de variable opcional se indica entre corchetes.


 from typing import Optional amount: int amount = None # Incompatible types in assignment (expression has type "None", variable has type "int") price: Optional[int] price = None 

Cualquier


A veces no desea limitar los posibles tipos de una variable. Por ejemplo, si realmente no es importante, o si planea hacer el procesamiento de diferentes tipos usted mismo. En este caso, puede usar la anotación Any . Mypy no jurará por el siguiente código:


 unknown_item: Any = 1 print(unknown_item) print(unknown_item.startswith("hello")) print(unknown_item // 0) 

Puede surgir la pregunta, ¿por qué no usar object ? Sin embargo, en este caso, se supone que aunque se puede transferir cualquier objeto, solo se puede acceder como una instancia de object .


 unknown_object: object print(unknown_object) print(unknown_object.startswith("hello")) # error: "object" has no attribute "startswith" print(unknown_object // 0) # error: Unsupported operand types for // ("object" and "int") 

Unión


Para los casos en que es necesario permitir el uso de no solo cualquier tipo, sino solo algunos, puede usar la anotación de typing.Union con una lista de tipos entre corchetes.


 def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0) hundreds(100) hundreds("100") # Argument 1 to "hundreds" has incompatible type "str"; expected "Union[int, float]" 

Por cierto, la anotación Optional[T] es equivalente a Union[T, None] , aunque no se recomienda dicha entrada.


Colecciones


El mecanismo de anotación de tipo admite el mecanismo genérico ( Genéricos , más en la segunda parte del artículo), que permite especificar los tipos de elementos almacenados en ellos para contenedores.


Listas


Para indicar que una variable contiene una lista, puede usar el tipo de lista como una anotación. Sin embargo, si desea especificar qué elementos contiene la lista, dicha anotación ya no será adecuada. Hay typing.List . typing.List para esto. Similar a cómo especificamos el tipo de una variable opcional, especificamos el tipo de elementos de la lista entre corchetes.


 titles: List[str] = ["hello", "world"] titles.append(100500) # Argument 1 to "append" of "list" has incompatible type "int"; expected "str" titles = ["hello", 1] # List item 1 has incompatible type "int"; expected "str" items: List = ["hello", 1] 

Se supone que la lista contiene un número indefinido de elementos del mismo tipo. Pero no hay restricciones en la anotación de un elemento: puede usar Any , Optional , List y otros. Si no se especifica el tipo de elemento, se supone que es Any .


Además de la lista, hay anotaciones similares para conjuntos: typing.Set y typing.FrozenSet .


Tuplas


Las tuplas, a diferencia de las listas, a menudo se usan para elementos heterogéneos. La sintaxis es similar con una diferencia: los corchetes indican el tipo de cada elemento de la tupla individualmente.


Si planea usar una tupla similar a la lista: almacenar un número desconocido de elementos del mismo tipo, puede usar puntos suspensivos ( ... ).


Anotación Tuple sin especificar tipos de elementos funciona de manera similar a Tuple[Any, ...]


 price_container: Tuple[int] = (1,) price_container = ("hello") # Incompatible types in assignment (expression has type "str", variable has type "Tuple[int]") price_container = (1, 2) # Incompatible types in assignment (expression has type "Tuple[int, int]", variable has type "Tuple[int]") price_with_title: Tuple[int, str] = (1, "hello") prices: Tuple[int, ...] = (1, 2) prices = (1, ) prices = (1, "str") # Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "Tuple[int, ...]") something: Tuple = (1, 2, "hello") 

Diccionarios


Para los diccionarios, typing.Dict usa typing.Dict . El tipo de clave y el tipo de valor se anotan por separado:


 book_authors: Dict[str, str] = {"Fahrenheit 451": "Bradbury"} book_authors["1984"] = 0 # Incompatible types in assignment (expression has type "int", target has type "str") book_authors[1984] = "Orwell" # Invalid index type "int" for "Dict[str, str]"; expected type "str" 

De manera similar, utiliza typing.DefaultDict y typing.OrderedDict


Resultado de la función


Puede usar cualquier anotación para indicar el tipo de resultado de la función. Pero hay algunos casos especiales.


Si una función no devuelve nada (por ejemplo, como print ), su resultado siempre es None . Para la anotación también usamos None .


Las opciones válidas para finalizar dicha función serían: explícitamente return None , return sin especificar un valor y terminar sin llamar a return .


 def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return "" # No return value expected else: pass 

Si la función nunca regresa (por ejemplo, como sys.exit ), debe usar la anotación NoReturn :


 def forever() -> NoReturn: while True: pass 

Si es una función generadora, es decir, su cuerpo contiene una yield , puede usar la anotación Iterable[T] o Generator[YT, ST, RT] para la función devuelta:


 def generate_two() -> Iterable[int]: yield 1 yield "2" # Incompatible types in "yield" (actual type "str", expected type "int") 

En lugar de una conclusión


Para muchas situaciones, hay tipos adecuados en el módulo de mecanografía, sin embargo, no consideraré todo, ya que el comportamiento es similar a los considerados.
Por ejemplo, hay un Iterator como la versión genérica para collections.abc.Iterator , typing.SupportsInt para indicar que el objeto admite el método __int__ , o Callable para funciones y objetos que admiten el método __call__


El estándar también define el formato de las anotaciones en forma de comentarios y archivos de código auxiliar que contienen información solo para analizadores estáticos.


En el próximo artículo me gustaría hacer hincapié en el mecanismo de trabajo de los genéricos y el procesamiento de anotaciones en tiempo de ejecución.

Source: https://habr.com/ru/post/es432656/


All Articles