Nouvelles annotations de type dans Python 3.8 (protocole, final, TypedDict, littéral)

Python 3.8 est sorti ce soir et les annotations de type ont de nouvelles fonctionnalités:


  • Protocoles
  • Dictionnaires dactylographiés
  • Spécificateur final
  • Correspondance de valeurs fixes

Si vous n'êtes pas encore familier avec les annotations de type, je vous recommande de faire attention à mes articles précédents ( début , suite )
Et tandis que tout le monde s'inquiète des morses, je veux parler brièvement des dernières nouveautés du module de frappe


Protocoles


Python utilise le typage canard et les classes ne sont pas tenues d'hériter d'une certaine interface, comme dans certains autres langages.
Malheureusement, avant la version 3.8, nous ne pouvions pas exprimer les exigences nécessaires pour un objet à l'aide d'annotations de type.
Le PEP 544 est conçu pour résoudre ce problème.


Des termes tels que «protocole itérateur» ou «protocole descripteur» sont déjà familiers et sont utilisés depuis longtemps.
Vous pouvez maintenant décrire les protocoles sous forme de code et vérifier leur conformité au stade de l'analyse statique.


Il est à noter qu'à partir de Python 3.6, le module de typage comprend déjà plusieurs protocoles standard.
Par exemple, SupportsInt (qui nécessite la méthode __int__ ), SupportsBytes (nécessite __bytes__ ) et quelques autres.


Description du protocole


Un protocole est décrit comme une classe régulière héritée de Protocol. Il peut avoir des méthodes (y compris celles avec implémentation) et des champs.
De vraies classes implémentant le protocole peuvent en hériter, mais ce n'est pas nécessaire.


 from abc import abstractmethod from typing import Protocol, Iterable class SupportsRoar(Protocol): @abstractmethod def roar(self) -> None: raise NotImplementedError class Lion(SupportsRoar): def roar(self) -> None: print("roar") class Tiger: def roar(self) -> None: print("roar") class Cat: def meow(self) -> None: print("meow") def roar_all(bigcats: Iterable[SupportsRoar]) -> None: for t in bigcats: t.roar() roar_all([Lion(), Tiger()]) # ok roar_all([Cat()]) # error: List item 0 has incompatible type "Cat"; expected "SupportsRoar" 

Nous pouvons combiner des protocoles en utilisant l'héritage, en créant de nouveaux.
Cependant, dans ce cas, vous devez également spécifier explicitement Protocol comme classe parent.


 class BigCatProtocol(SupportsRoar, Protocol): def purr(self) -> None: print("purr") 

Génériques, saisis automatiquement, appelables


Les protocoles, comme les classes régulières, peuvent être des génériques. Au lieu de spécifier Protocol et Generic[T, S,...] comme parents Generic[T, S,...] vous pouvez simplement spécifier Protocol[T, S,...]


Un autre type important de protocole est auto-typé (voir PEP 484 ). Par exemple


 C = TypeVar('C', bound='Copyable') class Copyable(Protocol): def copy(self: C) -> C: class One: def copy(self) -> 'One': ... 

De plus, les protocoles peuvent être utilisés lorsque la syntaxe d'annotation Callable n'est pas suffisante.
Décrivez simplement le protocole avec la méthode __call__ de la signature souhaitée


Contrôles d'exécution


Bien que les protocoles soient principalement conçus pour être utilisés par des analyseurs statiques, il est parfois nécessaire de vérifier si la classe appartient au protocole souhaité.
Pour rendre cela possible, appliquez le décorateur @runtime_checkable au protocole et les isinstance / issubclass commenceront à vérifier la conformité avec le protocole


Cependant, cette fonctionnalité a un certain nombre de restrictions d'utilisation. En particulier, les génériques ne sont pas pris en charge


Dictionnaires dactylographiés


Les classes (en particulier les classes de données ) ou les tuples nommés sont généralement utilisés pour représenter des données structurées.
mais parfois, par exemple, dans le cas d'une description de structure json, il peut être utile d'avoir un dictionnaire avec certaines clés.
PEP 589 introduit le concept de TypedDict , qui était auparavant disponible dans les extensions de mypy


Comme les classes de données ou les tuples typés, il existe deux façons de déclarer un dictionnaire typé. Par héritage ou en utilisant une usine:


 class Book(TypedDict): title: str author: str AlsoBook = TypedDict("AlsoBook", {"title": str, "author": str}) # same as Book book: Book = {"title": "Fareneheit 481", "author": "Bradbury"} # ok other_book: Book = {"title": "Highway to Hell", "artist": "AC/DC"} # error: Extra key 'artist' for TypedDict "Book" another_book: Book = {"title": "Fareneheit 481"} # error: Key 'author' missing for TypedDict "Book" 

Les dictionnaires typés prennent en charge l'héritage:


 class BookWithDesc(Book): desc: str 

Par défaut, toutes les clés de dictionnaire sont requises, mais vous pouvez désactiver cela en passant total=False lors de la création de la classe.
Cela ne s'applique qu'aux clés décrites dans le box-office actuel et n'affecte pas les hérités


 class SimpleBook(TypedDict, total=False): title: str author: str simple_book: SimpleBook = {"title": "Fareneheit 481"} # ok 

L'utilisation de TypedDict présente un certain nombre de limitations. En particulier:


  • les vérifications lors de l'exécution via isinstance ne sont pas prises en charge
  • les clés doivent être des littéraux ou des valeurs finales

De plus, des opérations dangereuses telles que .clear ou del interdites avec un tel dictionnaire.
Le travail sur une clé qui n'est pas un littéral peut également être interdit, car dans ce cas, il est impossible de déterminer le type de valeur attendu


Modificateur final


PEP 591 introduit le dernier modificateur (en tant que décorateur et annotation) à plusieurs fins


  • Désignation d'une classe dont il est impossible d'hériter:

 from typing import final @final class Childfree: ... class Baby(Childfree): # error: Cannot inherit from final class "Childfree" ... 

  • La désignation de la méthode interdite de passer outre:

 from typing import final class Base: @final def foo(self) -> None: ... class Derived(Base): def foo(self) -> None: # error: Cannot override final attribute "foo" (previously declared in base class "Base") ... 

  • Désignation d'une variable (paramètre de fonction. Champ de classe) dont la réattribution est interdite.

 ID: Final[float] = 1 ID = 2 # error: Cannot assign to final name "ID" SOME_STR: Final = "Hello" SOME_STR = "oops" # error: Cannot assign to final name "SOME_STR" letters: Final = ['a', 'b'] letters.append('c') # ok class ImmutablePoint: x: Final[int] y: Final[int] # error: Final name must be initialized with a value def __init__(self) -> None: self.x = 1 # ok ImmutablePoint().x = 2 # error: Cannot assign to final attribute "x" 

Dans ce cas, un code de la forme self.id: Final = 123 est autorisé, mais uniquement dans la méthode __init__


Littéral


Literal type défini dans PEP 586 est utilisé lorsque vous devez vérifier littéralement des valeurs spécifiques


Par exemple, Literal[42] signifie que seulement 42 est attendu comme valeur.
Il est important que non seulement l'égalité de la valeur soit vérifiée, mais aussi son type (par exemple, il ne sera pas possible d'utiliser False si 0 est attendu).


 def give_me_five(x: Literal[5]) -> None: pass give_me_five(5) # ok give_me_five(5.0) # error: Argument 1 to "give_me_five" has incompatible type "float"; expected "Literal[5]" give_me_five(42) # error: Argument 1 to "give_me_five" has incompatible type "Literal[42]"; expected "Literal[5]" 

Dans ce cas, plusieurs valeurs peuvent être transmises entre parenthèses, ce qui équivaut à utiliser Union (les types de valeurs peuvent ne pas coïncider).


Les expressions (par exemple, Literal[1+2] ) ou les valeurs de types mutables ne peuvent pas être utilisées comme valeurs.


À titre d'exemple utile, l'utilisation de Literal est la fonction open() , qui attend des valeurs de mode spécifiques.


Gestion des types lors de l'exécution


Si vous souhaitez traiter différents types d'informations (comme moi ) pendant l'exécution du programme,
Les fonctions get_origin et get_args sont désormais disponibles.


Ainsi, pour un type de la forme X[Y, Z,...] type X sera retourné comme origine, et (Y, Z, ...) comme arguments
Il convient de noter que si X est un alias pour le type intégré ou le type du module collections , il sera remplacé par l'original.


 assert get_origin(Dict[str, int]) is dict assert get_args(Dict[int, str]) == (int, str) assert get_origin(Union[int, str]) is Union assert get_args(Union[int, str]) == (int, str) 

Malheureusement, la fonction pour __parameters__ n'a pas


Les références


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


All Articles