
Creo que nos estamos acostumbrando lentamente al hecho de que Python tiene anotaciones de tipo: se les devolvieron dos versiones (3.5) en las anotaciones de funciones y métodos ( PEP 484 ), y en la última versión (3.6) a variables ( PEP 526 ).
Dado que estos dos PEP se inspiraron en MyPy , te diré qué alegrías mundanas y disonancias cognitivas me esperaban al usar este analizador estático, así como el sistema de escritura en su conjunto.
Descargo de responsabilidad: no planteo la cuestión de la necesidad o el daño del tipeo estático en Python. Solo estoy hablando de las trampas que encontré mientras trabajaba en un contexto de tipo estático.
Genéricos (mecanografía, genéricos)
Es bueno usar algo como List[int]
, Callable[[int, str], None]
en las anotaciones.
Es muy agradable cuando el analizador resalta el siguiente código:
T = ty.TypeVar('T') class A(ty.Generic[T]): value: T A[int]().value = 'str'
Sin embargo, ¿qué pasa si escribimos una biblioteca y el programador que la usa no usará un analizador estático?
¿Obligar al usuario a inicializar la clase con un valor y luego almacenar su tipo?
T = ty.TypeVar('T') class Gen(Generic[T]): value: T ref: Type[T] def __init__(self, value: T) -> None: self.value = value self.ref = type(value)
De alguna manera no es fácil de usar.
Pero, ¿y si quieres hacerlo?
b = Gen[A](B())
En busca de una respuesta a esta pregunta, me puse a typing
un poco y me sumergí en el mundo de las fábricas.

El hecho es que después de inicializar la instancia de la clase Genérica, obtiene el atributo __origin_class__
, que tiene el atributo __args__
, que es una tupla de tipo. Sin embargo, el acceso a él desde __init__
, así como desde __new__
, no lo es. Además, no está en la metaclase __call__
. Y el truco es que en el momento de la inicialización de la subclase de Generic
se convierte en otra metaclase _GenericAlias
, que establece el tipo final, ya sea después de que se inicializa el objeto, incluidos todos los métodos de su metaclase, o en el momento en que __getithem__
a __getithem__
. Por lo tanto, no hay forma de obtener tipos genéricos al construir un objeto.
Tiramos esta basura, prometimos una solución más universal.Por lo tanto, escribí un pequeño descriptor que resuelve este problema:
def _init_obj_ref(obj: 'Gen[T]') -> None: """Set object ref attribute if not one to initialized arg.""" if not hasattr(obj, 'ref'): obj.ref = obj.__orig_class__.__args__[0]
Por supuesto, en consecuencia, será necesario reescribir para un uso más universal, pero la esencia es clara.
[UPD]: Por la mañana decidí intentar hacer lo mismo que en el módulo de typing
, pero más simple:
import typing as ty T = ty.TypeVar('T') class A(ty.Generic[T]):
[UPD]: El desarrollador de typing
Ivan Levinsky dijo que ambas opciones podrían romperse de manera impredecible.
De todos modos, puedes usarlo de cualquier manera. Quizás __class_getitem__
es incluso un poco mejor, al menos __class_getitem__
es un método especial documentado (aunque su comportamiento para los genéricos no lo es).
Funciones y alias
Sí, los genéricos no son fáciles en absoluto:
Por ejemplo, si en algún lugar aceptamos una función como argumento, su anotación cambia automáticamente de covariante a contravariante:
class A: pass class B(A): pass def foo(arg: 'A') -> None:
Y en principio, no tengo quejas sobre la lógica, solo esto debe resolverse mediante alias genéricos:
TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None:
En general, la sección sobre la variabilidad de los tipos debe leerse cuidadosamente y más de una vez.
Compatibilidad con versiones anteriores
Esto no está tan de moda: desde la versión 3.7 Generic
es una subclase de ABCMeta
, que es muy conveniente y buena. Es malo que esto rompa el código si se está ejecutando en 3.6.
Herencia Estructural (Suptyping Stuctural)
Al principio estaba muy feliz: ¡se entregaron las interfaces! El rol de las interfaces lo realiza la clase Protocol
del módulo typing_extensions
, que, en combinación con el decorador @runtime
, le permite verificar si la clase implementa la interfaz sin herencia directa. MyPy también se destaca en un nivel más profundo.
Sin embargo, no noté muchos beneficios prácticos en tiempo de ejecución en comparación con la herencia múltiple.
Parece que el decorador solo verifica la presencia de un método con el nombre requerido, sin siquiera verificar el número de argumentos, sin mencionar la escritura:
import typing as ty import typing_extensions as te @te.runtime class IntStackP(te.Protocol): _list: ty.List[int] def push(self, val: int) -> None: ... class IntStack: def __init__(self) -> None: self._list: ty.List[int] = list() def push(self, val: int) -> None: if not isinstance(val, int): raise TypeError('wrong pushued val type') self._list.append(val) class StrStack: def __init__(self) -> None: self._list: ty.List[str] = list() def push(self, val: str, weather: ty.Any=None) -> None: if not isinstance(val, str): raise TypeError('wrong pushued val type') self._list.append(val) def push_func(stack: IntStackP, value: int): if not isinstance(stack, IntStackP): raise TypeError('is not IntStackP') stack.push(value) a = IntStack() b = StrStack() c: ty.List[int] = list() push_func(a, 1) push_func(b, 1)
Por otro lado, MyPy, a su vez, se comporta de manera más inteligente y destaca la incompatibilidad de los tipos:
push_func(a, 1) push_func(b, 1)
Sobrecarga del operador
Un tema muy nuevo, porque Al sobrecargar a los operadores con seguridad de tipo completo, toda la diversión desaparece. Esta pregunta ha aparecido repetidamente en el rastreador de errores MyPy, pero aún así se insulta en algunos lugares, y puede desactivarla de forma segura.
Les explico la situación:
class A: def __add__(self, other) -> int: return 3 def __iadd__(self, other) -> 'A': if isinstance(other, int): return NotImplemented return A() var = A() var += 3
Si el método de asignación compuesto devuelve NotImplemented
, Python primero busca __radd__
, luego usa __add__
y voila.
Lo mismo se aplica a la sobrecarga de cualquier método de subclase del formulario:
class A: def __add__(self, x : 'A') -> 'A': ... class B(A): @overload def __add__(self, x : 'A') -> 'A': ... @overload def __add__(self, x : 'B') -> 'B' : ...
En algunos lugares, las advertencias ya se han trasladado a la documentación, en algunos lugares todavía funcionan en la producción. Pero la conclusión general de los contribuyentes es dejar tales sobrecargas aceptables.