Novas anotações de tipo no Python 3.8 (Protocolo, Final, TypedDict, Literal)

O Python 3.8 saiu hoje à noite e as anotações de tipo têm novos recursos:


  • Protocolos
  • Dicionários digitados
  • Especificador final
  • Correspondência de valor fixo

Se você ainda não conhece as anotações de tipo, recomendo prestar atenção aos meus artigos anteriores ( início , continuação )
E enquanto todos estão preocupados com as morsas, quero falar brevemente sobre as últimas novidades do módulo de digitação


Protocolos


O Python usa digitação de pato e as classes não precisam herdar de uma certa interface, como em alguns outros idiomas.
Infelizmente, antes da versão 3.8, não era possível expressar os requisitos necessários para um objeto usando anotações de tipo.
O PEP 544 foi projetado para resolver esse problema.


Termos como "protocolo do iterador" ou "protocolo do descritor" já são familiares e são usados ​​há muito tempo.
Agora você pode descrever os protocolos na forma de código e verificar sua conformidade no estágio de análise estática.


Vale ressaltar que, a partir do Python 3.6, o módulo de digitação já inclui vários protocolos padrão.
Por exemplo, SupportsInt (que requer o método __int__ ), SupportsBytes (requer __bytes__ ) e alguns outros.


Descrição do protocolo


Um protocolo é descrito como uma classe regular herdada do protocolo. Pode ter métodos (incluindo aqueles com implementação) e campos.
Classes reais que implementam o protocolo podem herdar dele, mas isso não é necessário.


 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" 

Podemos combinar protocolos usando herança, criando novos.
No entanto, nesse caso, você também deve especificar explicitamente Protocolo como a classe pai.


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

Genéricos, de tipo próprio, que podem ser chamados


Protocolos, como classes regulares, podem ser genéricos. Em vez de especificar Protocol e Generic[T, S,...] como pai Generic[T, S,...] você pode simplesmente especificar Protocol[T, S,...]


Outro tipo importante de protocolo é auto-tipado (ver PEP 484 ). Por exemplo


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

Além disso, os protocolos podem ser usados ​​quando a sintaxe da anotação de Callable não é suficiente.
Apenas descreva o protocolo com o método __call__ da assinatura desejada


Verificações de tempo de execução


Embora os protocolos sejam projetados principalmente para uso de analisadores estáticos, às vezes é necessário verificar se a classe pertence ao protocolo desejado.
Para tornar isso possível, aplique o decorador @runtime_checkable ao protocolo e as verificações issubclass / issubclass começarão a verificar a conformidade com o protocolo


No entanto, esse recurso possui várias restrições de uso. Em particular, os genéricos não são suportados


Dicionários digitados


Classes (em particular, classes de dados ) ou tuplas nomeadas geralmente são usadas para representar dados estruturados.
mas às vezes, por exemplo, no caso de uma descrição da estrutura json, pode ser útil ter um dicionário com determinadas chaves.
O PEP 589 introduz o conceito de TypedDict , anteriormente disponível em extensões do mypy


Como classes de dados ou tuplas digitadas, há duas maneiras de declarar um dicionário digitado. Por herança ou usando uma fábrica:


 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" 

Dicionários digitados suportam herança:


 class BookWithDesc(Book): desc: str 

Por padrão, todas as chaves do dicionário são necessárias, mas você pode desativá-lo passando total=False ao criar a classe.
Isso se aplica apenas às chaves descritas nas bilheterias atuais e não afeta as informações herdadas.


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

O uso do TypedDict tem várias limitações. Em particular:


  • verificações em tempo de execução através de isinstance não são suportadas
  • chaves devem ser literais ou valores finais

Além disso, operações inseguras como .clear ou del proibidas nesse dicionário.
O trabalho em uma chave que não é literal também pode ser proibido, pois nesse caso é impossível determinar o tipo de valor esperado


Modificador final


O PEP 591 introduz o modificador final (como decorador e anotação) para diversos fins


  • Designação de uma classe da qual é impossível herdar:

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

  • A designação do método que é proibido de substituir:

 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") ... 

  • Designação de uma variável (parâmetro de função. Campo de classe), proibida a reatribuição.

 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" 

Nesse caso, um código no formato self.id: Final = 123 é permitido, mas apenas no método __init__


Literal


Literal tipo definido no PEP 586 é usado quando você precisa verificar literalmente valores específicos


Por exemplo, Literal[42] significa que apenas 42 é esperado como um valor.
É importante que não apenas a igualdade do valor seja verificada, mas também seu tipo (por exemplo, não será possível usar False se 0 for esperado).


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

Nesse caso, vários valores podem ser transmitidos entre colchetes, o que equivale a usar Union (os tipos de valores podem não coincidir).


Expressões (por exemplo, Literal[1+2] ) ou valores de tipos mutáveis ​​não podem ser usados ​​como valores.


Como um exemplo útil, usar Literal é a função open() , que espera valores de mode específicos.


Manipulação de tipo em tempo de execução


Se você deseja processar vários tipos de informações (como eu ) enquanto o programa está em execução,
As funções get_origin e get_args estão agora disponíveis.


Portanto, para um tipo do formato X[Y, Z,...] tipo X será retornado como origem e (Y, Z, ...) como argumentos
É importante notar que, se X for um alias para o tipo interno ou o tipo do módulo de collections , ele será substituído pelo 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) 

Infelizmente, a função para __parameters__ não foi


Referências


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


All Articles