Bienvenido, me llamo Luis y aquí les traigo este artículo.
Índice
Implementación de uniones, intersecciones, interfaces, genéricos y composición similares a Typecript en Python mediante protocolos y clases base abstractas
Muchos desarrolladores que desarrollan sistemas o servicios políglotas trabajarán con los lenguajes sospechosos habituales: Javascript, Python, Go, Java, por nombrar algunos.
El aumento sin precedentes de la popularidad de Typescript en los últimos años, debido en gran parte a su sintaxis y estilo suaves y hábiles, me ha llevado a escribir una guía para aquellos desarrolladores que generalmente se centran en los servicios de Typescript pero que ocasionalmente pueden necesitar trabajar en algo de Python aquellos que programan Python y quieren empezar a entender las tipificaciones más complejas que se incluyen en la biblioteca estándar typing
módulo.
Esta guía no profundizará en declaraciones de tipo base simples como cadenas o números, sino que se centrará en los tipos complejos y que a menudo no se aprenden, incluidos los tipos de unión, los tipos de intersección, los tipos componibles, la implementación implícita o explícita de interfaces y los genéricos. Tampoco irá más allá de la breve explicación de las diferencias entre los módulos de mecanografía de Typecript y Python que se describen a continuación, ya que los recursos adicionales para estos son abundantes y se pueden encontrar en muchas otras ubicaciones.
En resumen, Typescript es un lenguaje de superconjunto de Javascript que se compila en Javascript antes de la ejecución y se le quita la información de tipo (excepto en el Tiempo de ejecución de Deno), mientras que los tipos de Python son parte del Lenguaje Python y existen en tiempo de ejecución (incurriendo en un poco de sobrecarga en el proceso), lo que significa que se pueden inspeccionar en tiempo de ejecución utilizando el signature
objeto, que es parte del incorporado inspect
módulo. Los tipos de Python son utilizados por varias herramientas de análisis estático / de tipo para analizar su código; dichas herramientas no se explorarán en este artículo, aunque tenga en cuenta que he usado Mypy y Pylance para que este artículo genere los mensajes de lint.
Por último, el lado de Typecript de esta guía admite todas las versiones recientes de Typecript, mientras que el enfoque para el lado de Python se divide entre la implementación mediante abstract base classes
(como se introdujo en Python versión 3) y usando Protocols
(introducido en Python versión 3.8). Si el código en el que está trabajando usa 3.8 o posterior, no dude en probar cualquiera de los métodos (aunque recomiendo Protocols
), mientras que aquellos que trabajan en versiones anteriores se limitarán a la implementación a través de clases base abstractas únicamente, aunque recomiendo usar protocolos, ya que son una interfaz mucho más limpia que se acerca al estilo TypeScript.
Tipos de datos recursivos
Los análogos a estos tipos de clases de datos de Typecript en Python se encuentran usando el dataclasses
módulo, que se utiliza para implementar explícitamente tipos. Implementación sin dataclasses
se puede lograr utilizando clases estándar y el @property
decorador, aunque dataclasses
elimine gran parte del estándar de la construcción de clases de contenedor de datos simples.
A tener en cuenta: Clases de datos se introdujeron en Python 3.7, aunque existe una biblioteca de compatibilidad popular que se puede encontrar en pypi llamada dataclasses
que rellena el módulo de Python 3.6.
Para un enfoque implícito de la mecanografía, más parecido a Typecript, Python ofrece la Protocol
clase. Protocol
permite verificar los tipos implícitamente al verificar las coincidencias de la interfaz durante la asignación de tipos, aunque solo verifica métodos y variables y no los tipos directamente.
Sin embargo, tenga en cuenta que Protocol
solo se introdujo en Python 3.8, por lo que si está en una versión anterior, tendrá que recurrir a métodos explícitos y usar dataclasses
o las implementaciones de clases más detalladas.
Como se muestra en lo anterior, tuve que usar el PersonProtocol
en lugar del recursivo Person
escriba para la propiedad de amigos del Person
clase. Esto se debe a un problema en el mypy
herramienta. Si estas usando pylance
para el análisis de tipo, puede utilizar el Person
como el tipo recursivo.
Mientras que el uso de tipos implícitos a través de Protocol
puede parecer más detallado, la oportunidad es mucho mayor en bases de código grandes donde no tiene que importar directamente tipos a través de los límites del servicio, y es mejor cuando se usa una biblioteca en la que no tiene acceso a las clases subyacentes que recurren al tipo pato.
Tipos de unión
Los tipos de unión permiten que la variable adopte una forma de cualquiera de los tipos enumerados en la unión. Esto es particularmente útil para restringir funciones genéricas a solo unos pocos tipos, o para variables que podrían tomar una de varias formas.
TypeScript tiene un operador directo para esto, mediante el cual el uso de la tubería |
implica que el tipo es una union.
Como las mecanografías de Python no se compilan, deben permanecer dentro de los límites del lenguaje, por lo que en lugar de un operador debe importar el genérico Union
del módulo de mecanografía que conduce a una implementación más detallada.
Cabe señalar también que cualquier tipo opcional es de hecho un tipo de unión que significa: el opcionalOptional[int]
es equivalente a la uniónUnion[int, None]
en Python y el opcionala?: number
es equivalente a la unión undefined
en TypeScript, donde la primera sintaxis más corta para ambos idiomas es simplemente azúcar sintáctica para el último.
Tipos de intersección
Los tipos de intersección se utilizan para combinar especificaciones de interfaz y, a menudo, se utilizan como una alternativa a la herencia de clases o como una mejora de la escritura de pato.
Los tipos de intersección en Python carecen actualmente de un enfoque simplista (con debates de larga data sobre si deberían incluirse o no). Por ahora, al menos, debe construir clases de ‘contenedor’ que se extiendan fuera de los tipos que desea intersectar. Como antes, hay dos formas de hacer esto: una explícita mediante el uso de dataclasses
, y el otro uso implícito Protocols
.
Intersección explícita usando clases de datos con una clase de contenedor
El dataclass
enfoque requiere una clase contenedora con un método de inicialización engorroso que reenvía los argumentos a los __init__
métodos de clases de datos correctos.
Intersección implícita usando protocolos con una clase de contenedor
Esto se simplifica en el Protocol
enfoque basado, con la clase de contenedor simplemente envolviendo los protocolos base.
Genéricos
Con cualquier nivel de complejidad, se encontrará escribiendo funciones o interfaces genéricas que ofrecen utilidad en tipos genéricos.
A continuación se muestran los enfoques para implementar una función de tipo genérico único, así como una función de tipo multigénico para comparar las especificaciones del lenguaje para esto.
Nuevamente, debido a que las mecanografías de Python no se compilan, encontramos que el enfoque de Python es un poco más detallado al tener que declarar los tipos dentro del cuerpo del código antes de usarlos como argumentos genéricos.
Interfaces
Las interfaces en TypeScript se utilizan para definir los métodos y propiedades del objeto sin indicar explícitamente qué tipo debería ser el objeto, ofreciendo un contrato sintáctico que el objeto debería implementar.
Curiosamente, Python no tiene un análogo directo a interfaces como Typecript. Python primero optó por implementar una variante de clases base abstractas (como se encuentra en Java y C ++) disponible a través de la biblioteca estándar abc
(disponible desde Python 2.7).
Las clases base abstractas son una característica muy rica y avanzada en Python, por lo que no entraré en detalles sobre qué más pueden ofrecer las clases aquí. Se puede encontrar más información relacionada con el uso de abc en los documentos de Python con el fundamento de abc y cómo se relacionan con las interfaces que se encuentran en PEP 3119.
Clases base abstractas
Utilizando la ABC
clase base con el abstractmethod
El decorador requiere que cualquier clase heredada de la clase abstracta sobrescriba cualquier método que esté decorado con la abstractmethod
decorador durante la instanciación. Esto ofrece ciertas utilidades en tiempo de ejecución, ya que esencialmente obliga a los usuarios de las clases abstractas a implementar los métodos. Como las clases heredan de las clases abstractas explícitamente, puede comprobar durante el tiempo de ejecución si son instancias de las clases abstractas directamente.
También tenga en cuenta aquí que en lugar de pass
, donde se usa comúnmente para denotar funciones que no están implementadas, elijo usar el tipo de elipsis incorporado …
para indicar que los métodos son métodos simulados. Esto también se encuentra comúnmente (aunque no necesariamente estándar) en simulacros de tipo de bibliotecas externas.
El enfoque de estilo ABC tiene comprobaciones en tiempo de ejecución al inicializar las subclases, sin embargo, las subclases deben extenderse desde estas clases base ABC.
Protocolos
El protocolo tiene la desventaja de que no hay comprobaciones de inicialización en tiempo de ejecución, sin embargo, las clases no necesitan extenderse fuera del protocolo, lo que permite una programación mucho más genérica. Se pueden realizar comprobaciones con estas interfaces de protocolo con isinstance(X, FileHandler)
en tiempo de ejecución (aunque solo si está decorado con el @runtime_checkable
como a continuación) para asegurarse X
implementa los métodos (aunque no verifica los tipos de argumentos de los tipos de retorno).
Composición y extensión
A menudo, querrá construir sobre otra interfaz y aumentarla con propiedades o métodos adicionales. Esto se puede lograr en TypeScript mediante el uso de intersections
o extendiéndose desde otro interface
como se demuestra a continuación.
Similar a los métodos de intersección para Python, nuevamente hay dos formas de hacer esto en Python: una a través de clases base abstractas y la otra a través de protocolos.
Clases base abstractas
Debido al requisito de uso del @abstractmethod
decorador en ABC, y el hecho de que no funcionan en la sintaxis de atributo de clase pública más corta, debe declarar los atributos de clase pública detalladamente con @property
decorador y un método de propiedad como se muestra a continuación.
La composición se logra mediante la herencia basada en clases.
Protocolos
En lugar de utilizar el @property
decorador como se requiere con el uso del @abstractmethod
en ABCs
, en su lugar podemos utilizar el estilo mucho más limpio y menos detallado de declarar atributos de clase pública a través de la <name>:<type>
sintaxis.
La herencia de otras clases base de Protocolo se realiza utilizando el mismo enfoque que se encuentra en el intersección sección anterior, que conduce a la composibilidad a través de la herencia.
Tipos híbridos
Estos son tipos útiles que amplían las clases que se utilizan con regularidad (como las funciones) y las aumentan con funciones o propiedades adicionales. Los siguientes fragmentos crean un objeto Dog que se puede usar como una función, pero también tiene una propiedad adicional type
que representa la raza del perro.
En Python se puede lograr lo mismo mediante el uso de Protocols
o clases de bases abstractas (omitidas por simplicidad).
Conversión de tipo implícita vs explícita
A menudo, se encuentra trabajando con variables que tienen el mismo tipo de base pero que deben interpretarse de manera muy diferente. Ejemplos clásicos de esto son: tiempo (segundo o milisegundo), temperatura (Celsius o Fahrenheit) o moneda (USD o AUD).
TypeScript realiza conversiones implícitas entre tipos de forma automática, como se ve en el siguiente ejemplo, lo que genera complicaciones en torno a tipos de variables simples.
Las conversiones de tipo implícitas de TypeScript pueden causar problemas cuando las variables del mismo tipo base deben interpretarse de manera muy diferente, como las unidades Fahrenheit y Celsius.
Python tiene una construcción llamada NewType
que previene las conversiones implícitas cuando se usa para construir tipos con nombre y logra exactamente lo que buscamos.
Para lograr estas mismas propiedades en TypeScript, necesita un tipo un poco más complejo, que suele ser un buen lugar para enums
para ser aplicado.
type Celsius = { type: "celsius"; value: number; }; const celsius = (value: number): Celsius => ({ value, type: "celsius", }); type Fahrenheit = { type: "fahrenheit"; value: number; }; const fahrenheit = (value: number): Fahrenheit => ({ value, type: "fahrenheit", }); type Temperature = Celsius | Fahrenheit; function convertToCelsius(value: Fahrenheit): Celsius { return celsius((value.value * 9) / 5 + 32); } function convertToFahrenheit(value: Celsius): Fahrenheit { return fahrenheit(((value.value - 32) * 5) / 9); } const converted = convertToCelsius(0); // Argument of type '0' is not assignable to parameter of type 'Fahrenheit' const fahrenheitValue: Fahrenheit = fahrenheit(32); convertToFahrenheit(fahrenheitValue); // Argument of type 'Fahrenheit' is not assignable to parameter of type 'Celsius'.
Resumiendo
A través de esta guía, espero demostrar que puede adoptar los mismos enfoques que puede que ya esté haciendo con TypeScript y trasladar estas ideas a las bases de código de Python. En particular, aumentando el tipo de pato de Python mediante la implementación de interfaces, explícitamente mediante el uso de abstract base classes
, o más preferiblemente a través de la implementación de interfaz implícita utilizando el nuevo Protocol
característica introducida en Python 3.8.
Si tiene alguna pregunta relacionada con esta guía, no dude en dejarla a continuación.
Añadir comentario