Una forma de subclase NamedTuple para fines de comprobación de tipo

Tengo varias variables con nombre que comparten algunos campos. Tengo una función que acepta estas tuplas y se garantiza que solo interactúe con los campos compartidos. Quiero teclear el código en mypy.

Un ejemplo del código sería:

from typing import NamedTuple class Base(NamedTuple): x: int y: int class BaseExtended(NamedTuple): x: int y: int z: str def DoSomething(tuple: Base): return tuple.x + tuple.y base = Base(3, 4) base_extended = BaseExtended(5, 6, 'foo') DoSomething(base) DoSomething(base_extended) 

Cuando ejecuto mypy en este código obtengo un error predecible:

mypy_example.py:20: error: el argumento 1 a “DoSomething” tiene un tipo incompatible “BaseExtended”; esperado “Base”

¿No hay forma de estructurar mi código y mantener la comprobación de tipos de mypy? No puedo heredar BaseExtended de Base, ya que hay un error en la implementación de la herencia NamedTuple:

https://github.com/python/typing/issues/427

Tampoco quiero usar un feo “Unión [Base, Base Extendida]”, ya que esto se interrumpe cuando trato de revisar una Lista, ya que “Lista [Unión [Base, Base Extendida]]” no es igual a “Lista [BaseExtendida ] “debido a un poco de magia mypy sobre los tipos de variante / covariante:

https://github.com/python/mypy/issues/3351

¿Debo simplemente abandonar la idea?

La forma en que se construyen las tuplas con nombre hacen que la herencia se typing.NamedTuple clases aún no son posibles. Tendría que escribir su propia metaclase para extender la clase typing.NamedTupleMeta para hacer que las subclases funcionen, e incluso entonces la clase generada por collections.namedtuple() simplemente no está diseñada para extenderse .

En su lugar, desea utilizar el nuevo módulo de clases de datos para definir sus clases y lograr la herencia:

 from dataclasses import dataclass @dataclass(frozen=True) class Base: x: int y: int @dataclass(frozen=True) class BaseExtended(Base): z: str 

El módulo es nuevo en Python 3.7, pero puede pip install dataclasses el backport en Python 3.6.

Lo anterior define dos clases inmutables con atributos x e y , con la clase BaseExtended agregando un atributo más. BaseExtended es una subclase completa de Base , por lo que para fines de escritura DoSomething() los requisitos de la función DoSomething() .

Las clases no son tuplas con nombre completo, ya que no tienen una longitud ni son compatibles con la indexación, pero se agregan trivialmente al crear una clase base que hereda de collections.abc.Sequence , agregando dos métodos para acceder a los campos por índice:

 from collections.abc import Sequence from dataclasses import dataclass, fields class DataclassSequence(Sequence): # make a dataclass tuple-like by accessing fields by index def __getitem__(self, i): return getattr(self, fields(self)[i].name) def __len__(self): return len(fields(self)) @dataclass(frozen=True) class Base(DataclassSequence): x: int y: int 

MyPy pronto dataclasses explícitamente las dataclasses ; en la versión 0.600 obtendrá errores aún ya que no reconoce la importación del módulo de datos o que se genera un método __new__ .

En Python 3.6 y attrs anteriores, también puede instalar el proyecto attrs para lograr los mismos efectos; la clase base de secuencia anterior se ve así usando attrs :

 from collections.abc import Sequence import attr class AttrsSequence(Sequence): # make a dataclass tuple-like by accessing fields by index def __getitem__(self, i): return getattr(self, attr.fields(type(self))[i].name) def __len__(self): return len(attr.fields(type(self))) @attr.s(frozen=True, auto_attribs=True) class Base(AttrsSequence): x: int y: int 

dataclasses se basa directamente en los attrs , con los attrs proporcionan más funcionalidad; mypy soporta totalmente las clases generadas con attrs .

Hay un PEP 544 que propone una extensión al sistema de tipos que permitirá el subtipo estructural (tipificación estática del pato). También la implementación en tiempo de ejecución de typing.NamedTuple se mejorará pronto, probablemente en Python 3.6.2 a finales de junio (esto también se incluirá en la parte posterior de la typing en PyPI).