Bonnes pratiques concernant les annotations¶
- auteur
Larry Hastings
Résumé
Ce document a pour but de regrouper les bonnes pratiques de travail avec le dictionnaire d'annotations. Si vous écrivez du code Python qui examine les __annotations__
des objets, nous vous encourageons à suivre les recommandations décrites dans la suite.
Ce document est organisé en quatre sections : bonnes pratiques pour accéder aux annotations d'un objet en Python 3.10 et plus récent, bonnes pratiques pour accéder aux annotations d'un objet en Python 3.9 et antérieur, autres bonnes pratiques pour __annotations__
à appliquer quelle que soit la version de Python, et enfin les curiosités concernant __annotations__
.
Notez que ce document traite spécifiquement du traitement des __annotations__
, et non de l'utilisation des annotations. Si vous cherchez des informations sur la façon d'utiliser les « indications de type » dans votre code, veuillez consulter le module typing
.
Accès au dictionnaire des annotations d'un objet dans Python 3.10 et plus récent¶
Python 3.10 ajoute une nouvelle fonction à la bibliothèque standard :
inspect.get_annotations()
. Dans les versions Python 3.10 et plus récentes, l'appel à cette fonction est la meilleure pratique pour accéder au dictionnaire d'annotations de tout objet qui prend en charge les annotations. Cette fonction peut également convertir pour vous les annotations contenues dans des chaînes de caractères en objets.Si pour une raison quelconque,
inspect.get_annotations()
n'est pas viable pour votre cas d'utilisation, vous pouvez accéder à l'attribut de données__annotations__
manuellement. La bonne pratique pour cela a également changé en Python 3.10 : à partir de Python 3.10, le fonctionnement deo.__annotations__
est toujours garanti sur les fonctions, classes et modules Python. Si vous êtes certain que l'objet que vous examinez est l'un de ces trois objets spécifiques, vous pouvez simplement utilisero.__annotations__
pour accéder au dictionnaire d'annotations de l'objet.Cependant, d'autres types d'objets appelables – par exemple, les objets appelables créés par
functools.partial()
– peuvent ne pas avoir d'attribut__annotations__
défini. Pour accéder à l'attribut__annotations__
d'un objet éventuellement inconnu, la meilleure pratique, à partir de la version 3.10 de Python, est d'appelergetattr()
avec trois arguments, par exemplegetattr(o, '__annotations__', None)
.Dans les versions antérieures à Python 3.10, l'accès aux
__annotations__
d'une classe qui n'a pas d'annotation mais dont un parent de cette classe en a, aurait renvoyé les__annotations__
de la classe parent. Dans les versions 3.10 et plus récentes, le résultat d'annotations de la classe enfant est un dictionnaire vide.
Accès au dictionnaire des annotations d'un objet en Python 3.9 et antérieur¶
En Python 3.9 et antérieur, l'accès au dictionnaire des annotations d'un objet est beaucoup plus compliqué que dans les versions plus récentes. Le problème est dû à un défaut de conception de ces anciennes versions de Python, notamment en ce qui concerne les annotations de classe.
La bonne pratique pour accéder au dictionnaire d'annotations d'autres objets – fonctions, autres objets appelables et modules – est la même que pour la 3.10, en supposant que vous n'appelez pas
inspect.get_annotations()
: vous devez utiliser la forme à trois arguments degetattr()
pour accéder à l'attribut__annotations__
de l'objet.Malheureusement, ce n'est pas la bonne pratique pour les classes. Le problème est que, puisque
__annotations__
est optionnel sur les classes et que les classes peuvent hériter des attributs de leurs classes de base, accéder à l'attribut__annotations__
d'une classe peut par inadvertance renvoyer le dictionnaire d'annotations d'une classe de base. Par exemple :class Base: a: int = 3 b: str = 'abc' class Derived(Base): pass print(Derived.__annotations__)Ceci affiche le dictionnaire d'annotations de
Base
, pas deDerived
.Votre code doit gérer les différentes options si l'objet que vous examinez est une classe (
isinstance(o, type)
). Dans ce cas, la bonne pratique repose sur un détail d'implémentation de Python 3.9 et antérieur : si une classe a des annotations définies, elles sont stockées dans le dictionnaire__dict__
de la classe. Puisque la classe peut avoir ou non des annotations définies, la bonne pratique est d'appeler la méthodeget
sur le dictionnaire de la classe.Pour résumer, voici un exemple de code qui accède en toute sécurité à l'attribut
__annotations__
d'un objet quelconque en Python 3.9 et antérieur :if isinstance(o, type): ann = o.__dict__.get('__annotations__', None) else: ann = getattr(o, '__annotations__', None)Après avoir exécuté ce code,
ann
pourra être soit un dictionnaire, soitNone
. Nous vous conseillons de vérifier le type deann
en utilisantisinstance()
avant de poursuivre l'analyse.Notez que certains objets de type exotique ou malformé peuvent ne pas avoir d'attribut
__dict__
donc, pour plus de sécurité, vous pouvez également utilisergetattr()
pour accéder à__dict__
.
Conversion manuelle des annotations contenues dans une chaîne de caractères¶
Dans les situations où certaines annotations peuvent se présenter sous forme de chaînes de caractères brutes, et que vous souhaitez évaluer ces chaînes pour trouver les valeurs Python qu'elles représentent, il est vraiment préférable d'appeler
inspect.get_annotations()
pour faire ce travail à votre place.Si vous utilisez Python 3.9 ou antérieur ou, si pour une raison quelconque, vous ne pouvez pas utiliser
inspect.get_annotations()
, vous devrez dupliquer son principe de fonctionnement. Nous vous encourageons à examiner l'implémentation deinspect.get_annotations()
dans la version actuelle de Python et à suivre une approche similaire.En bref, si vous souhaitez évaluer une annotation empaquetée en chaîne de caractères sur un objet arbitraire
o
:
Si
o
est un module, utilisezo.__dict__
pour accéder aux nomsglobals
lors de l'appel àeval()
.Si
o
est une classe, utilisezsys.modules[o.__module__].__dict__
pour désigner les nomsglobals
, etdict(vars(o))
pour désigner leslocals
, lorsque vous appelezeval()
.Si
o
est un appelable encapsulé à l'aide defunctools.update_wrapper()
,functools.wraps()
, oufunctools.partial()
, dés-encapsulez-le itérativement en accédant ào.__wrapped__
ouo.func
selon le cas, jusqu'à ce que vous ayez trouvé la fonction racine.Si
o
est un objet appelable (mais pas une classe), utilisezo.__globals__
pour désigner les globales lors de l'appel deeval()
.Cependant, toutes les valeurs de chaîne utilisées comme annotations ne peuvent pas être transformées avec succès en valeurs Python par
eval()
. Les valeurs de chaîne peuvent théoriquement contenir des chaînes valides et, en pratique, il existe des cas d'utilisation valables pour les indications de type qui nécessitent d'annoter avec des valeurs de chaîne qui ne peuvent pas être évaluées. Par exemple :
Les types d'union de style PEP 604 avec
|
, avant que cette prise en charge ne soit ajoutée à Python 3.10.Les définitions qui ne sont pas nécessaires à l'exécution, importées uniquement lorsque
typing.TYPE_CHECKING
est vrai.Si
eval()
tente d'évaluer de telles valeurs, elle échoue et lève une exception. Ainsi, lors de la conception d'une API de bibliothèque fonctionnant avec des annotations, il est recommandé de ne tenter d'évaluer des valeurs de type chaîne que si l'appelant le demande explicitement.
Bonnes pratiques pour __annotations__
dans toutes les versions de Python¶
Évitez d'assigner directement des objets à l'élément
__annotations__
. Laissez Python gérer le paramétrage de__annotations__
.Si vous assignez directement à l'élément
__annotations__
d'un objet, vous devez toujours le définir comme undict
.Si vous accédez directement à l'élément
__annotations__
d'un objet, vous devez vous assurer qu'il s'agit d'un dictionnaire avant de tenter d'examiner son contenu.Évitez de modifier les dictionnaires
__annotations__
.Évitez de supprimer l'attribut
__annotations__
d'un objet.
Les curiosités concernant __annotations__
¶
Dans toutes les versions de Python 3, les fonctions créent paresseusement un dictionnaire d'annotations si aucune annotation n'est définie sur cet objet. Vous pouvez supprimer l'attribut
__annotations__
en utilisantdel fn.__annotations__
, mais si vous accédez ensuite àfn.__annotations__
, l'objet créera un nouveau dictionnaire vide qu'il stockera et renverra quand on lui demande ses annotations. Si vous supprimez les annotations d'une fonction avant qu'elle n'ait créé paresseusement son dictionnaire d'annotations, vous obtiendrez une exceptionAttributeError
; si vous utilisezdel fn.__annotations__
deux fois de suite, vous êtes sûr de toujours obtenirAttributeError
.Tout ce qui est dit dans le paragraphe précédent s'applique également aux objets de type classe et module en Python 3.10 et suivants.
Dans toutes les versions de Python 3, vous pouvez définir à
None
l'élément__annotations__
sur un objet fonction. Cependant, si vous accédez ensuite aux annotations de cet objet en utilisantfn.__annotations__
, un dictionnaire vide sera créé paresseusement, comme indiqué dans le premier paragraphe de cette section. Ce n'est pas le cas des modules et des classes, quelle que soit la version de Python ; ces objets permettent de définir__annotations__
à n'importe quelle valeur Python, et conserveront la valeur définie.Si Python convertit vos annotations en chaînes de caractères (en utilisant
from __future__ import annotations
), et que vous spécifiez une chaîne de caractères comme annotation, la chaîne sera elle-même entre guillemets. En fait, l'annotation est mise entre guillemets deux fois. Par exemple :from __future__ import annotations def foo(a: "str"): pass print(foo.__annotations__)Ceci renvoie :
{'a': "'str'"}
. Cela ne devrait pas vraiment être considéré comme une « bizarrerie » ; nous le mentionnons ici simplement parce que cela peut être surprenant.