Python 3.8 salió esta noche y las anotaciones de tipo obtuvieron nuevas características:
- Protocolos
- Diccionarios mecanografiados
- Especificador final
- Coincidencia de valor fijo
Si aún no está familiarizado con las anotaciones de tipo, le recomiendo que preste atención a mis artículos anteriores ( comienzo , continuación )
Y aunque todos están preocupados por las morsas, quiero hablar brevemente sobre lo último en el módulo de escritura
Protocolos
Python utiliza la escritura de pato y no se requiere que las clases hereden de una determinada interfaz, como en algunos otros idiomas.
Desafortunadamente, antes de la versión 3.8, no podíamos expresar los requisitos necesarios para un objeto usando anotaciones de tipo.
PEP 544 está diseñado para resolver este problema.
Términos como "protocolo iterador" o "protocolo descriptor" ya son familiares y se han utilizado durante mucho tiempo.
Ahora puede describir los protocolos en forma de código y verificar su cumplimiento en la etapa de análisis estático.
Vale la pena señalar que, comenzando con Python 3.6, el módulo de mecanografía ya incluye varios protocolos estándar.
Por ejemplo, SupportsInt
(que requiere el método __int__
), SupportsBytes
(requiere __bytes__
) y algunos otros.
Descripción del protocolo
Un protocolo se describe como una clase regular que hereda de Protocol. Puede tener métodos (incluidos aquellos con implementación) y campos.
Las clases reales que implementan el protocolo pueden heredar de él, pero esto no es necesario.
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()])
Podemos combinar protocolos usando herencia, creando nuevos.
Sin embargo, en este caso, también debe especificar explícitamente el Protocolo como la clase principal.
class BigCatProtocol(SupportsRoar, Protocol): def purr(self) -> None: print("purr")
Genéricos, autotipados, invocables
Los protocolos, como las clases regulares, pueden ser genéricos. En lugar de especificar Protocol
y Generic[T, S,...]
como padres Generic[T, S,...]
simplemente puede especificar Protocol[T, S,...]
Otro tipo importante de protocolo es el autoescrito (ver PEP 484 ). Por ejemplo
C = TypeVar('C', bound='Copyable') class Copyable(Protocol): def copy(self: C) -> C: class One: def copy(self) -> 'One': ...
Además, se pueden usar protocolos cuando la sintaxis de anotación Callable
no es suficiente.
Simplemente describa el protocolo con el método __call__
de la firma deseada
Verificaciones de tiempo de ejecución
Aunque los protocolos están diseñados principalmente para su uso por analizadores estáticos, a veces es necesario verificar si la clase pertenece al protocolo deseado.
Para hacer esto posible, aplique el decorador @runtime_checkable
al protocolo y las isinstance
/ issubclass
comenzarán a verificar el cumplimiento del protocolo
Sin embargo, esta función tiene varias restricciones de uso. En particular, los genéricos no son compatibles
Diccionarios mecanografiados
Las clases (en particular, las clases de datos ) o las tuplas con nombre se usan generalmente para representar datos estructurados.
pero a veces, por ejemplo, en el caso de una descripción de estructura json, puede ser útil tener un diccionario con ciertas teclas.
PEP 589 presenta el concepto de TypedDict
, que anteriormente estaba disponible en extensiones de mypy
Al igual que las clases de datos o las tuplas escritas, hay dos formas de declarar un diccionario escrito. Por herencia o usando una fábrica:
class Book(TypedDict): title: str author: str AlsoBook = TypedDict("AlsoBook", {"title": str, "author": str})
Los diccionarios escritos admiten herencia:
class BookWithDesc(Book): desc: str
Por defecto, se requieren todas las claves del diccionario, pero puede deshabilitar esto pasando total=False
al crear la clase.
Esto se aplica solo a las claves descritas en la taquilla actual y no afecta a las heredadas
class SimpleBook(TypedDict, total=False): title: str author: str simple_book: SimpleBook = {"title": "Fareneheit 481"}
Usar TypedDict
tiene una serie de limitaciones. En particular:
- las comprobaciones en tiempo de ejecución a través de isinstance no son compatibles
- las claves deben ser literales o valores finales
Además, tales operaciones inseguras como .clear
o del
prohibidas con dicho diccionario.
También se puede prohibir trabajar en una clave que no sea literal, ya que en este caso es imposible determinar el tipo de valor esperado
Modificador final
PEP 591 presenta el modificador final (como decorador y anotación) para varios propósitos
- Designación de una clase de la que es imposible heredar:
from typing import final @final class Childfree: ... class Baby(Childfree):
- La designación del método que tiene prohibido anular:
from typing import final class Base: @final def foo(self) -> None: ... class Derived(Base): def foo(self) -> None:
- Designación de una variable (parámetro de función. Campo de clase), que está prohibido reasignar.
ID: Final[float] = 1 ID = 2
En este caso, se permite un código de la forma self.id: Final = 123
, pero solo en el método __init__
Literal
Literal
tipo definido en PEP 586 se usa cuando necesita verificar literalmente valores específicos
Por ejemplo, Literal[42]
significa que solo se espera 42 como valor.
Es importante que no solo se verifique la igualdad del valor, sino también su tipo (por ejemplo, no será posible usar False si se espera 0).
def give_me_five(x: Literal[5]) -> None: pass give_me_five(5)
En este caso, se pueden transmitir varios valores entre paréntesis, lo que equivale a utilizar Union (los tipos de valores pueden no coincidir).
Las expresiones (por ejemplo, Literal[1+2]
) o valores de tipos mutables no se pueden usar como valores.
Como un ejemplo útil, usar Literal
es la función open()
, que espera valores de mode
específicos.
Manejo de tipos en tiempo de ejecución
Si desea procesar varios tipos de información (como yo ) mientras se ejecuta el programa,
Las funciones get_origin y get_args ahora están disponibles.
Entonces, para un tipo de la forma X[Y, Z,...]
tipo X
se devolverá como origen, y (Y, Z, ...)
como argumentos
Vale la pena señalar que si X es un alias para el tipo incorporado o el tipo del módulo de collections
, entonces será reemplazado por el 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)
Desafortunadamente, la función para __parameters__
no
Referencias