Une introduction aux annotations de type Python. Continuation


Illustration de Magdalena Tomczyk


Dans la première partie de l'article, j'ai décrit les bases de l'utilisation des annotations de type. Cependant, plusieurs points importants n'ont pas été pris en compte. Premièrement, les génériques sont un mécanisme important, et deuxièmement, il peut parfois être utile de trouver des informations sur les types attendus lors de l'exécution. Mais je voulais commencer avec des choses plus simples


Annonce préalable


En règle générale, vous ne pouvez pas utiliser un type avant sa création. Par exemple, le code suivant ne démarre même pas:


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

Pour résoudre ce problème, il est permis d'utiliser un littéral de chaîne. Dans ce cas, les annotations seront calculées différées.


 class LinkedList: data: Any next: 'LinkedList' 

Vous pouvez également accéder aux classes à partir d'autres modules (bien sûr, si le module est importé): some_variable: 'somemodule.SomeClass'


Remarque

De manière générale, toute expression calculable peut être utilisée comme annotation. Cependant, il est recommandé de les garder aussi simples que possible afin que les utilitaires d'analyse statique puissent les utiliser. En particulier, ils ne comprendront probablement pas les types calculables dynamiquement. Pour en savoir plus sur les restrictions, cliquez ici: PEP 484 - Astuces de type # Astuces de type acceptables


Par exemple, le code suivant fonctionnera et même des annotations seront disponibles lors de l'exécution, mais mypy générera une erreur dessus


 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, il est prévu d'inclure le calcul d'annotation de type différé ( PEP 563 ), qui supprimera cette technique avec les littéraux de chaîne. avec Python 3.7, vous pouvez activer un nouveau comportement à l'aide de la construction d' from __future__ import annotations


Fonctions et objets appelés


Pour les situations où vous devez transférer une fonction ou un autre objet (par exemple, en tant que rappel), vous devez utiliser l'annotation Callable [[ArgType1, ArgType2, ...], ReturnType]
Par exemple


 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]" 

Il est permis de spécifier uniquement le type de retour de la fonction sans spécifier ses paramètres. Dans ce cas, les points de suspension sont utilisés: Callable[..., ReturnType] . Notez qu'il n'y a pas de crochets autour des points de suspension.


Pour le moment, il est impossible de décrire une signature de fonction avec un nombre variable de paramètres d'un certain type ou de spécifier des arguments nommés.


Types génériques


Parfois, il est nécessaire de sauvegarder des informations sur un type, sans le fixer de manière rigide. Par exemple, si vous écrivez un conteneur qui stocke les mêmes données. Ou une fonction qui renvoie des données du même type que l'un des arguments.


Des types tels que List ou Callable, que nous avons vu précédemment, utilisent simplement le mécanisme générique. Mais en plus des types standard, vous pouvez créer vos propres types génériques. Pour ce faire, premièrement, obtenez une variable TypeVar, qui sera un attribut du générique, et deuxièmement, déclarez directement un type générique:


 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" 

Comme vous pouvez le voir, pour les types génériques, l'inférence automatique du type de paramètre fonctionne.


Si nécessaire, un générique peut avoir un nombre quelconque de paramètres: Generic[T1, T2, T3] .


De plus, lors de la définition de TypeVar, vous pouvez restreindre les types valides:


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

Cast


Parfois, l'analyseur statique de l'analyseur ne peut pas déterminer correctement le type de variable, dans ce cas, vous pouvez utiliser la fonction cast. Sa seule tâche est de montrer à l'analyseur que l'expression est d'un type particulier. Par exemple:


 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]) 

Il peut également être utile pour les décorateurs:


 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" 

Travailler avec des annotations d'exécution


Bien que l'interpréteur n'utilise pas seul les annotations, elles sont disponibles pour votre code pendant l'exécution du programme. Pour cela, un __annotations__ objet __annotations__ est __annotations__ qui contient un dictionnaire avec les annotations indiquées. Pour les fonctions, il s'agit des annotations de paramètres et du type de retour, pour un objet, des annotations de champ, de la portée globale, des variables et de leurs annotations.


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

get_type_hints est également disponible - il renvoie des annotations pour l'objet qui lui est transmis, dans de nombreuses situations, il correspond au contenu de __annotations__ , mais il existe des différences: il ajoute également des annotations des objets parents (dans l'ordre inverse de __mro__ ), et permet également des déclarations préliminaires de types spécifiés en tant que chaînes.


 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]} 

Pour les types génériques, des informations sur le type lui-même et ses paramètres sont disponibles via les __origin__ et __args__ , mais cela ne fait pas partie de la norme et le comportement a déjà changé entre les versions 3.6 et 3.7

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


All Articles