Una introducción a las anotaciones de tipo Python. Continuación


Ilustración de Magdalena Tomczyk.


En la primera parte del artículo, describí los conceptos básicos del uso de anotaciones de tipo. Sin embargo, varios puntos importantes no fueron considerados. En primer lugar, los genéricos son un mecanismo importante y, en segundo lugar, a veces puede ser útil encontrar información sobre los tipos esperados en tiempo de ejecución. Pero quería comenzar con cosas más simples


Anuncio preliminar


Por lo general, no puede usar un tipo antes de que se cree. Por ejemplo, el siguiente código ni siquiera comenzará:


class LinkedList: data: Any next: LinkedList # NameError: name 'LinkedList' is not defined 

Para solucionar esto, está permitido usar un literal de cadena. En este caso, las anotaciones se calcularán diferidas.


 class LinkedList: data: Any next: 'LinkedList' 

También puede acceder a clases desde otros módulos (por supuesto, si el módulo se importa): some_variable: 'somemodule.SomeClass'


Observación

En términos generales, cualquier expresión computable puede usarse como una anotación. Sin embargo, se recomienda que se mantengan lo más simples posible para que las utilidades de análisis estático puedan usarlos. En particular, lo más probable es que no entiendan los tipos dinámicamente computables. Lea más sobre las restricciones aquí: PEP 484 - Sugerencias de tipos # Sugerencias de tipos aceptables


Por ejemplo, el siguiente código funcionará e incluso las anotaciones estarán disponibles en tiempo de ejecución, sin embargo, mypy arrojará un error


 def get_next_type(arg=None): if arg: return LinkedList else: return Any class LinkedList: data: Any next: 'get_next_type()' # error: invalid type comment or annotation 

UPD : en Python 4.0, está previsto habilitar el cálculo de anotaciones de tipo retrasado ( PEP 563 ), que eliminará esta técnica con literales de cadena. con Python 3.7 puede habilitar un nuevo comportamiento usando la construcción de from __future__ import annotations


Funciones y objetos llamados


Para situaciones en las que necesita transferir una función u otro objeto (por ejemplo, como devolución de llamada), debe usar la anotación invocable [[ArgType1, ArgType2, ...], ReturnType]
Por ejemplo


 def help() -> None: print("This is help string") def render_hundreds(num: int) -> str: return str(num // 100) def app(helper: Callable[[], None], renderer: Callable[[int], str]): helper() num = 12345 print(renderer(num)) app(help, render_hundreds) app(help, help) # error: Argument 2 to "app" has incompatible type "Callable[[], None]"; expected "Callable[[int], str]" 

Está permitido especificar solo el tipo de retorno de la función sin especificar sus parámetros. En este caso, se utiliza la elipsis: Callable[..., ReturnType] . Tenga en cuenta que no hay corchetes alrededor de los puntos suspensivos.


Por el momento, es imposible describir una firma de función con un número variable de parámetros de cierto tipo o especificar argumentos con nombre.


Tipos genéricos


A veces es necesario guardar información sobre un tipo, sin arreglarlo rígidamente. Por ejemplo, si escribe un contenedor que almacena los mismos datos. O una función que devuelve datos del mismo tipo que uno de los argumentos.


Tipos como List o Callable, que vimos anteriormente, solo usan el mecanismo genérico. Pero además de los tipos estándar, puede crear sus propios tipos genéricos. Para hacer esto, en primer lugar, obtenga TypeVar una variable que será un atributo del genérico y, en segundo lugar, declare directamente un tipo genérico:


 T = TypeVar("T") class LinkedList(Generic[T]): data: T next: "LinkedList[T]" def __init__(self, data: T): self.data = data head_int: LinkedList[int] = LinkedList(1) head_int.next = LinkedList(2) head_int.next = 2 # error: Incompatible types in assignment (expression has type "int", variable has type "LinkedList[int]") head_int.data += 1 head_int.data.replace("0", "1") # error: "int" has no attribute "replace" head_str: LinkedList[str] = LinkedList("1") head_str.data.replace("0", "1") head_str = LinkedList[str](1) # error: Argument 1 to "LinkedList" has incompatible type "int"; expected "str" 

Como puede ver, para los tipos genéricos, funciona la inferencia automática del tipo de parámetro.


Si es necesario, un genérico puede tener cualquier número de parámetros: Generic[T1, T2, T3] .


Además, al definir TypeVar, puede restringir los tipos válidos:


 T2 = TypeVar("T2", int, float) class SomethingNumeric(Generic[T2]): pass x = SomethingNumeric[str]() # error: Value of type variable "T2" of "SomethingNumeric" cannot be "str" 

Elenco


A veces, el analizador estático no puede determinar correctamente el tipo de variable, en este caso, puede utilizar la función de conversión. Su única tarea es mostrarle al analizador que la expresión es de un tipo particular. Por ejemplo:


 from typing import List, cast def find_first_str(a: List[object]) -> str: index = next(i for i, x in enumerate(a) if isinstance(x, str)) return cast(str, a[index]) 

También puede ser útil para decoradores:


 MyCallable = TypeVar("MyCallable", bound=Callable) def logged(func: MyCallable) -> MyCallable: @wraps(func) def wrapper(*args, **kwargs): print(func.__name__, args, kwargs) return func(*args, **kwargs) return cast(MyCallable, wrapper) @logged def mysum(a: int, b: int) -> int: return a + b mysum(a=1) # error: Missing positional argument "b" in call to "mysum" 

Trabajar con anotaciones de tiempo de ejecución


Aunque el intérprete no utiliza anotaciones por sí solo, están disponibles para su código mientras se ejecuta el programa. Para esto, se __annotations__ un __annotations__ objeto __annotations__ que contiene un diccionario con las anotaciones indicadas. Para funciones, estas son anotaciones de parámetros y tipo de retorno, para un objeto, anotaciones de campo, para alcance global, variables y sus anotaciones.


 def render_int(num: int) -> str: return str(num) print(render_int.annotations) # {'num': <class 'int'>, 'return': <class 'str'>} 

get_type_hints también está disponible: devuelve anotaciones para el objeto que se le pasa, en muchas situaciones coincide con el contenido de __annotations__ , pero hay diferencias: también agrega anotaciones de los objetos principales (en el orden inverso de __mro__ ), y también permite declaraciones preliminares de tipos especificados como cadenas.


 T = TypeVar("T") class LinkedList(Generic[T]): data: T next: "LinkedList[T]" print(LinkedList.__annotations__) # {'data': ~T, 'next': 'LinkedList[T]'} print(get_type_hints(LinkedList)) # {'data': ~T, 'next': __main__.LinkedList[~T]} 

Para los tipos genéricos, la información sobre el tipo en sí y sus parámetros está disponible a través de los __origin__ y __args__ , pero esto no es parte del estándar y el comportamiento ya ha cambiado entre las versiones 3.6 y 3.7

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


All Articles