Introduction aux annotations de type Python

Présentation



Illustration de Magdalena Tomczyk


Deuxième partie


Python est un langage à typage dynamique et nous permet de manipuler librement des variables de différents types. Cependant, lors de l'écriture de code, d'une manière ou d'une autre, nous supposons quels types de variables seront utilisés (cela peut être dû à une restriction de l'algorithme ou de la logique métier). Et pour le bon fonctionnement du programme, il est important pour nous de trouver le plus tôt possible les erreurs liées au transfert de données de mauvais type.


Gardant l'idée d'un canard de frappe dynamique dans les versions modernes de Python (3.6+), il prend en charge les annotations de types de variables, les champs de classe, les arguments et les valeurs de retour des fonctions:



Les annotations de type sont simplement lues par l'interpréteur Python et ne sont plus traitées, mais peuvent être utilisées à partir d'un code tiers et sont principalement conçues pour être utilisées par des analyseurs statiques.


Je m'appelle Andrey Tikhonov et je suis engagé dans le développement backend chez Lamoda.


Dans cet article, je veux expliquer les bases de l'utilisation des annotations de type et considérer les exemples typiques implémentés en typing annotations.


Outils d'annotation


Les annotations de type sont prises en charge par de nombreux IDE Python qui mettent en évidence un code incorrect ou fournissent des conseils lors de la frappe.


Par exemple, voici à quoi cela ressemble dans Pycharm:


Erreur de mise en évidence



Astuces:



Les annotations de type sont également traitées par les linters de console.


Voici la sortie de pylint:


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

Mais pour le même fichier que mypy a trouvé:


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

Le comportement de différents analyseurs peut varier. Par exemple, mypy et pycharm gèrent le changement du type d'une variable différemment. Plus loin dans les exemples, je me concentrerai sur la sortie de mypy.


Dans certains exemples, le code peut s'exécuter sans exception au démarrage, mais peut contenir des erreurs logiques dues à l'utilisation de variables de type incorrect. Et dans certains exemples, il peut même ne pas être exécuté.


Les bases


Contrairement aux anciennes versions de Python, les annotations de type ne sont pas écrites dans les commentaires ou la docstring, mais directement dans le code. D'une part, cela rompt la compatibilité descendante, d'autre part, cela signifie clairement qu'il fait partie du code et peut être traité en conséquence


Dans le cas le plus simple, l'annotation contient le type directement attendu. Des cas plus complexes seront discutés ci-dessous. Si la classe de base est spécifiée comme une annotation, le passage d'instances de ses descendants en tant que valeurs est acceptable. Cependant, vous ne pouvez utiliser que les fonctionnalités implémentées dans la classe de base.


Les annotations pour les variables sont écrites après les deux points après l'identifiant. Après cela, la valeur peut être initialisée. Par exemple


 price: int = 5 title: str 

Les paramètres de fonction sont annotés de la même manière que les variables, et la valeur de retour est indiquée après la flèche -> et avant les deux-points finaux. Par exemple


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

Pour les champs de classe, les annotations doivent être spécifiées explicitement lors de la définition d'une classe. Cependant, les analyseurs peuvent les générer automatiquement en fonction de la méthode __init__ , mais dans ce cas, ils ne seront pas disponibles au moment de l'exécution. En savoir plus sur l'utilisation des annotations lors de l'exécution dans la deuxième partie de l'article


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

Par ailleurs, lors de l'utilisation de la classe de données, les types de champ doivent être spécifiés dans la classe. En savoir plus sur la classe de données


Types intégrés


Bien que vous puissiez utiliser des types standard comme annotations, de nombreuses choses utiles sont cachées dans le module de typing .


En option


Si vous marquez la variable avec le type int et essayez de lui affecter None , il y aura une erreur:


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


Dans de tels cas, une annotation Optional avec un type spécifique est fournie dans le module de saisie. Veuillez noter que le type de variable facultative est indiqué entre crochets.


 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 

Tout


Parfois, vous ne voulez pas limiter les types possibles de variable. Par exemple, si ce n'est vraiment pas important, ou si vous prévoyez de faire vous-même le traitement de différents types. Dans ce cas, vous pouvez utiliser l'annotation Any . Mypy ne jure pas sur le code suivant:


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

La question peut se poser, pourquoi ne pas utiliser object ? Cependant, dans ce cas, il est supposé que même si n'importe quel objet peut être transféré, il ne peut être consulté qu'en tant qu'instance d' 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") 

Union


Pour les cas où il est nécessaire d'autoriser l'utilisation non seulement de n'importe quel type, mais seulement de certains, vous pouvez utiliser l'annotation typing.Union avec une liste de types entre crochets.


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

Soit dit en passant, l'annotation Optional[T] est équivalente à Union[T, None] , bien qu'une telle entrée ne soit pas recommandée.


Les collections


Le mécanisme d'annotation de type prend en charge le mécanisme générique ( Generics , plus dans la deuxième partie de l'article), qui permet de spécifier les types d'éléments stockés pour les conteneurs.


Listes


Afin d'indiquer qu'une variable contient une liste, vous pouvez utiliser le type de liste comme annotation. Cependant, si vous souhaitez spécifier quels éléments la liste contient, une telle annotation ne sera plus appropriée. Il y a de la typing.List . Semblable à la façon dont nous avons spécifié le type d'une variable facultative, nous spécifions le type des éléments de liste entre crochets.


 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] 

On suppose que la liste contient un nombre indéfini d'éléments du même type. Mais il n'y a aucune restriction sur l'annotation d'un élément: vous pouvez utiliser Any , Optional , List et autres. Si le type d'élément n'est pas spécifié, il est supposé être Any .


En plus de la liste, des annotations similaires typing.Set aux ensembles: typing.Set et typing.FrozenSet .


Tuples


Les tuples, contrairement aux listes, sont souvent utilisés pour des éléments hétérogènes. La syntaxe est similaire avec une différence: les crochets indiquent le type de chaque élément du tuple individuellement.


Si vous prévoyez d'utiliser un tuple similaire à la liste: stocker un nombre inconnu d'éléments du même type, vous pouvez utiliser les points de suspension ( ... ).


Le Tuple annotation sans spécifier les types d'élément fonctionne de la même manière que le 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") 

Dictionnaires


Pour les dictionnaires, typing.Dict utilisé. Le type de clé et le type de valeur sont annotés séparément:


 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" 

Utilisé de la même manière typing.DefaultDict et typing.OrderedDict


Résultat de la fonction


Vous pouvez utiliser n'importe quelle annotation pour indiquer le type de résultat de la fonction. Mais il y a quelques cas particuliers.


Si une fonction ne renvoie rien (par exemple, comme print ), son résultat est toujours None . Pour l'annotation, nous utilisons également None .


Les options valides pour terminer une telle fonction seraient: renvoyer explicitement None , retourner sans spécifier de valeur et terminer sans appeler 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 fonction ne retourne jamais (par exemple, comme sys.exit ), vous devez utiliser l'annotation NoReturn :


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

S'il s'agit d'une fonction de générateur, c'est-à-dire que son corps contient une yield , vous pouvez utiliser l'annotation Iterable[T] ou Generator[YT, ST, RT] pour la fonction renvoyée:


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

Au lieu d'une conclusion


Pour de nombreuses situations, il existe des types appropriés dans le module de frappe, mais je ne considérerai pas tout, car le comportement est similaire à ceux considérés.
Par exemple, il existe un Iterator comme version générique pour collections.abc.Iterator , typing.SupportsInt pour indiquer que l'objet prend en charge la méthode __int__ , ou Callable pour les fonctions et les objets qui prennent en charge la méthode __call__


La norme définit également le format des annotations sous forme de commentaires et de fichiers de raccord qui contiennent des informations uniquement pour les analyseurs statiques.


Dans le prochain article je voudrais m'attarder sur le mécanisme de travail des génériques et le traitement des annotations en runtime.

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


All Articles