Python 3.5 a introduit le nouveau module de typage qui fournit une prise en charge standard de la bibliothèque pour tirer parti des annotations de fonctions pour les indicateurs de type facultatifs. Cela ouvre la porte à de nouveaux outils intéressants pour la vérification de type statique comme mypy et, éventuellement, à une optimisation automatisée basée sur un type. Les indications de type sont spécifiées dans PEP-483 et PEP-484.
Dans ce tutoriel, j'explore les possibilités offertes par les astuces de type et explique comment utiliser mypy pour analyser statiquement vos programmes Python et améliorer de manière significative la qualité de votre code.
Les indicateurs de type sont construits au-dessus des annotations de fonctions. En bref, les annotations de fonction vous permettent d'annoter les arguments et de renvoyer la valeur d'une fonction ou d'une méthode avec des métadonnées arbitraires. Les indications de type sont un cas particulier d'annotation de fonction qui annotent spécifiquement les arguments de la fonction et la valeur de retour avec des informations de type standard. Les annotations de fonctions en général et les indications de type en particulier sont totalement facultatives. Jetons un coup d'oeil à un exemple rapide:
"python def reverse_slice (texte: str, début: int, fin: int) -> str: texte retourné [début: fin] [:: - 1]
reverse_slice ('abcdef', 3, 5) 'ed "
Les arguments ont été annotés avec leur type ainsi que la valeur de retour. Mais il est essentiel de réaliser que Python l'ignore complètement. Il rend les informations de type disponibles via le annotations attribut de l'objet fonction, mais c'est à peu près tout.
python reverse_slice .__ annotations 'end': int, 'return': str, 'start': int, 'text': str
Pour vérifier que Python ignore vraiment les indications de type, nous allons tout gâcher:
"python def reverse_slice (texte: float, début: str, fin: bool) -> dict: texte de retour [début: fin] [:: - 1]
reverse_slice ('abcdef', 3, 5) 'ed "
Comme vous pouvez le constater, le code se comporte de la même manière, indépendamment des indications de type..
D'ACCORD. Les indications de type sont facultatives. Les indications de type sont totalement ignorées par Python. Quel est le but d'eux, alors? Eh bien, il y a plusieurs bonnes raisons:
Je vais plonger dans l'analyse statique avec Mypy plus tard. Le support IDE a déjà commencé avec le support de PyCharm 5 pour les indicateurs de type. La documentation standard est idéale pour les développeurs qui peuvent facilement comprendre le type d'arguments et la valeur renvoyée simplement en examinant une signature de fonction ainsi que des générateurs de documentation automatisés capables d'extraire les informations de type à partir des astuces..
dactylographie
ModuleLe module de saisie contient des types conçus pour prendre en charge les indications de type. Pourquoi ne pas simplement utiliser les types Python existants tels que int, str, list et dict? Vous pouvez certainement utiliser ces types, mais en raison du typage dynamique de Python, au-delà des types de base, vous n’obtenez pas beaucoup d’informations. Par exemple, si vous souhaitez spécifier qu'un argument peut être un mappage entre une chaîne et un entier, il n'existe aucun moyen de le faire avec des types Python standard. Avec le module de dactylographie, c'est aussi simple que:
Cartographie python [str, int]
Regardons un exemple plus complet: une fonction qui prend deux arguments. L'un d'eux est une liste de dictionnaires où chaque dictionnaire contient des clés qui sont des chaînes et des valeurs des entiers. L'autre argument est une chaîne ou un entier. Le module de typage permet une spécification précise de tels arguments compliqués.
"python à partir de la liste d'importation importée, Dict, Union
def foo (a: Liste [Dict [str, int]], b: Union [str, int]) -> int: "" "Imprimer une liste de dictionnaires et renvoyer le nombre de dictionnaires" "" si isinstance (b, str): b = int (b) pour i dans la plage (b): print (a)
x = [dict (a = 1, b = 2), dict (c = 3, d = 4)] foo (x, '3')
['b': 2, 'a': 1, 'd': 4, 'c': 3] ['b': 2, 'a': 1, 'd': 4 , 'c': 3] ['b': 2, 'a': 1, 'd': 4, 'c': 3] "
Voyons quelques-uns des types les plus intéressants du module de frappe.
Le type Callable vous permet de spécifier la fonction qui peut être passée en tant qu'argument ou renvoyée en conséquence, car Python traite les fonctions comme des citoyens de première classe. La syntaxe de callables est de fournir un tableau de types d'arguments (à nouveau issus du module de typage) suivi d'une valeur de retour. Si cela prête à confusion, voici un exemple:
"python def do_something_fancy (data: Set [float], on_error: Callable [[Exception, int], None]):…
"
La fonction de rappel on_error est spécifiée en tant que fonction qui prend une exception et un entier en tant qu'arguments et ne renvoie rien..
Le type Tout signifie qu'un vérificateur de type statique doit autoriser toute opération ainsi que l'affectation à tout autre type. Chaque type est un sous-type de Any.
Le type d'union que vous avez vu précédemment est utile lorsqu'un argument peut avoir plusieurs types, ce qui est très courant en Python. Dans l'exemple suivant, le verify_config () function accepte un argument de configuration, qui peut être un objet de configuration ou un nom de fichier. Si c'est un nom de fichier, il appelle une autre fonction pour analyser le fichier dans un objet de configuration et le renvoyer.
"python def verify_config (config: Union [str, Config]): si estinstance (config, str): config = parse_config_file (config)…
def parse_config_file (nom du fichier: str) -> Config:…
"
Le type optionnel signifie que l'argument peut aussi être None. Facultatif [T]
est équivalent à Union [T, Aucun]
Il existe de nombreux autres types qui désignent diverses fonctionnalités telles que Iterable, Iterator, Réversible, SupportsInt, SupportsFloat, Sequence, MutableSequence et IO. Consultez la documentation du module de dactylographie pour la liste complète.
L'essentiel est que vous puissiez spécifier le type d'arguments d'une manière très fine prenant en charge le système de types Python avec une haute fidélité et autorisant également les classes de base génériques et abstraites..
Parfois, vous souhaitez faire référence à une classe dans une indication de type dans l'une de ses méthodes. Par exemple, supposons que la classe A puisse effectuer une opération de fusion prenant une autre instance de A, fusionne avec elle-même et renvoie le résultat. Voici une tentative naïve d'utiliser des indicateurs de type pour le spécifier:
"classe python A: fusion de def (autre: A) -> A:…
1 classe A: ----> 2 fusion de fusion (autre: A = Aucune) -> A: 3… 4
NameError: le nom 'A' n'est pas défini "
Qu'est-il arrivé? La classe A n'est pas encore définie lorsque l'indice de type de sa méthode merge () est vérifié par Python. Par conséquent, la classe A ne peut pas être utilisée à ce stade (directement). La solution est assez simple, et je l’ai déjà vue utilisée par SQLAlchemy. Vous spécifiez simplement l'indicateur de type sous forme de chaîne. Python comprendra que c'est une référence avancée et fera le bon choix:
classe python A: def merge (autre: 'A' = Aucun) -> 'A':…
L'utilisation d'indications de type pour les spécifications de type longues présente un inconvénient: elle peut encombrer le code et le rendre moins lisible, même s'il fournit de nombreuses informations sur le type. Vous pouvez créer des alias comme n'importe quel autre objet. C'est aussi simple que:
"Données python = Dict [int, Séquence [Dict [str, Facultatif [Liste [float]]]]]
def foo (data: Data) -> bool:… "
get_type_hints ()
Fonction d'assistanceLe module de typage fournit la fonction get_type_hints (), qui fournit des informations sur les types d'argument et la valeur de retour. Tandis que le annotations attribut retourne des indications de type car il ne s'agit que d'annotations, je vous recommande néanmoins d'utiliser la fonction get_type_hints () car elle résout les références en aval. De plus, si vous spécifiez la valeur par défaut de None sur l'un des arguments, la fonction get_type_hints () renverra automatiquement son type sous la forme Union [T, NoneType] si vous venez de spécifier T. Voyons la différence à l'aide de la méthode A.merge () défini précédemment:
"impression python (A.merge.annotations)
'autre': 'A', 'retour': 'A' "
le annotations attribut renvoie simplement la valeur d'annotation telle quelle. Dans ce cas, il s'agit simplement de la chaîne 'A' et non de l'objet de classe A, auquel 'A' est simplement une référence en aval.
"print python (get_type_hints (A.merge))
'revenir':
La fonction get_type_hints () a converti le type du autre argument à une Union de A (la classe) et NoneType en raison de l'argument par défaut None. Le type de retour a également été converti en classe A.
Les conseils de type sont une spécialisation des annotations de fonctions. Ils peuvent également fonctionner côte à côte avec d'autres annotations de fonctions..
Pour ce faire, le module de frappe fournit deux décorateurs: @no_type_check et @no_type_check_decorator. le @no_type_check Le décorateur peut être appliqué à une classe ou à une fonction. Il ajoute le no_type_check attribut à la fonction (ou à chaque méthode de la classe). De cette façon, les vérificateurs de type sauront ignorer les annotations, qui ne sont pas des indicateurs de type..
C'est un peu fastidieux, car si vous écrivez une bibliothèque qui sera utilisée largement, vous devez supposer qu'un vérificateur de type sera utilisé, et si vous souhaitez annoter vos fonctions avec des astuces non typées, vous devez également les décorer avec @no_type_check.
Un scénario courant lorsqu’on utilise des annotations de fonctions régulières consiste également à avoir un décorateur qui les exploite. Vous souhaitez également désactiver la vérification de type dans ce cas. Une option consiste à utiliser le @no_type_check décorateur en plus de votre décorateur, mais cela vieillit. Au lieu de cela, le @no_Type_check_decorator peut être utilisé pour décorer votre décorateur afin qu'il se comporte également comme @no_type_check (ajoute le no_type_check attribut).
Permettez-moi d'illustrer tous ces concepts. Si vous essayez de get_type_hint () (comme tout vérificateur de type le fera) sur une fonction annotée avec une annotation de chaîne normale, la méthode get_type_hints () l'interprétera comme une référence aval:
"python def f (a: 'une annotation'): pass
print (get_type_hints (f))
SyntaxError: ForwardRef doit être une expression - a obtenu une "annotation"
Pour l’éviter, ajoutez le décorateur @no_type_check et get_type_hints renvoie simplement un dict vide, tandis que le __annotations__ attribut renvoie les annotations:
"python @no_type_check def f (a: 'une annotation'): pass
print (get_type_hints (f))
imprimer (f.annotations) 'a': 'une annotation' "
Supposons maintenant que nous ayons un décorateur qui imprime le dict d'annotations. Vous pouvez le décorer avec le @no_Type_check_decorator et ensuite décorer la fonction et ne pas s'inquiéter d'un vérificateur de type appelant get_type_hints () et confus. C’est probablement une pratique recommandée pour tous les décorateurs qui utilisent des annotations. N'oublie pas le @ functools.wraps, sinon les annotations ne seront pas copiées dans la fonction décorée et tout s'effondrera. Ceci est couvert en détail dans Python 3 Function Annotations.
python @no_type_check_decorator def print_annotations (f): @ functools.wraps (f) def Decored (* arguments, ** kwargs): print (f .__ annotations__) retourne f (* args, ** kwargs) renvoie décoré
Maintenant, vous pouvez décorer la fonction juste avec @print_annotations, et chaque fois qu'il est appelé, il imprimera ses annotations.
"python @print_annotations def f (a: 'une annotation'): pass
f (4) 'a': 'une annotation' "
Appel get_type_hints () est également sûr et retourne un dict vide.
Python print (get_type_hints (f))
Mypy est un vérificateur de type statique qui a inspiré les indications de type et le module de frappe. Guido van Rossum est lui-même l'auteur de PEP-483 et co-auteur de PEP-484..
Mypy est en développement très actif et à ce jour, le paquet sur PyPI est obsolète et ne fonctionne pas avec Python 3.5. Pour utiliser Mypy avec Python 3.5, obtenez les dernières nouvelles du référentiel de Mypy sur GitHub. C'est aussi simple que:
bash pip3 installez git + git: //github.com/JukkaL/mypy.git
Une fois que vous avez installé Mypy, vous pouvez simplement exécuter Mypy sur vos programmes. Le programme suivant définit une fonction qui attend une liste de chaînes. Il appelle ensuite la fonction avec une liste d'entiers.
"python à partir de la liste d'importation importée
def case_insensitive_dedupe (data: List [str]): "" "Convertit toutes les valeurs en minuscules et supprime les doublons" "" retourne la liste (set (x.lower () pour x dans les données))
print (case_insensitive_dedupe ([1, 2])) "
Lors de l'exécution du programme, il échoue évidemment au moment de l'exécution avec l'erreur suivante:
plain python3 dedupe.py Traceback (l'appel le plus récent en dernier): fichier "dedupe.py", ligne 8, dans
Quel est le problème avec ça? Le problème est que, même dans ce cas très simple, la cause première n’est pas claire. Est-ce un problème de type d'entrée? Ou peut-être le code lui-même est faux et ne devrait pas essayer d'appeler le inférieur() méthode sur l'objet 'int'. Un autre problème est que si vous n'avez pas une couverture de test à 100% (et, soyons honnête, aucun d'entre nous n'en a), alors de tels problèmes peuvent se dissimuler dans certains chemins de code non testés et rarement utilisés et être détectés au pire moment de la production..
La saisie statique, aidée par les indications de type, vous procure un filet de sécurité supplémentaire en veillant à toujours appeler vos fonctions (annotées avec des indications de type) avec les types appropriés. Voici la sortie de Mypy:
plain (N)> mypy dedupe.py dedupe.py:8: erreur: l'élément de liste 0 a le type incompatible "int" dedupe.py:8: erreur: l'élément de liste 1 a le type incompatible "int" dedupe.py:8: erreur : L'élément de liste 2 a un type incompatible "int"
Ceci est simple, pointe directement vers le problème et n'exige pas beaucoup de tests. Un autre avantage de la vérification de type statique est que, si vous vous y engagez, vous pouvez ignorer la vérification de type dynamique, sauf lors de l'analyse d'une entrée externe (lecture de fichiers, demandes réseau entrantes ou entrée utilisateur). Cela crée également beaucoup de confiance en ce qui concerne le refactoring.
Les conseils de frappe et le module de frappe sont des ajouts totalement facultatifs à l’expressivité de Python. S'ils ne conviennent pas à tous les goûts, ils peuvent être indispensables pour les grands projets et les grandes équipes. La preuve est que les grandes équipes utilisent déjà la vérification de type statique. Maintenant que les informations de type sont normalisées, il sera plus facile de partager le code, les utilitaires et les outils qui les utilisent. Des IDE comme PyCharm en tirent déjà parti pour offrir une meilleure expérience de développement.