Способ подкласса NamedTuple для целей проверки типов

У меня есть несколько именованных кортежей, которые разделяют некоторые поля. У меня есть функция, которая принимает эти кортежи и гарантированно взаимодействует только с общими полями. Я хочу, чтобы проверить такой код в mypy.

Пример кода будет:

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)

Когда я запускаю mypy для этого кода, я получаю предсказуемую ошибку:

mypy_example.py:20: ошибка: аргумент 1 для "DoSomething" имеет несовместимый тип "BaseExtended"; ожидаемая "база"

Нет ли способа структурировать мой код и поддерживать проверку типов mypy? Я не могу наследовать BaseExtended от Base, поскольку в реализации наследования NamedTuple есть ошибка:

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

Я также не хочу использовать некрасивый "Union [Base, BaseExtended]", так как он ломается, когда я пытаюсь проверить тип List, поскольку "List [Union [Base, BaseExtended]]" не равен "List [BaseExtended" ] "из-за некоторого количества волшебства о вариантах/ковариантных типах:

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

Должен ли я просто отказаться от идеи?

Ответ 1

Способ создания именованных кортежей делает наследование от классов typing.NamedTuple пока невозможным. Вам нужно написать свой собственный метакласс, чтобы расширить класс typing.NamedTupleMeta, чтобы заставить работать подклассы, и даже тогда класс, сгенерированный collections.namedtuple(), просто не создан для расширения.

Вместо этого вы хотите использовать новый модуль dataclasses для определения ваших классов и достижения наследования:

from dataclasses import dataclass

@dataclass(frozen=True)
class Base:
    x: int
    y: int

@dataclass(frozen=True)
class BaseExtended(Base):
    z: str

Модуль является новым в Python 3.7, но вы можете pip install dataclasses задний порт в Python 3.6.

Выше определены два неизменных класса с атрибутами x и y, с классом BaseExtended, добавляющим еще один атрибут. BaseExtended является полным подклассом Base, поэтому для целей набора текста соответствует требованиям для функции DoSomething().

Классы не являются полными именованными кортежами, поскольку они не имеют длины или не поддерживают индексацию, но они тривиально добавляются путем создания базового класса, который наследуется от collections.abc.Sequence, добавляя два метода для доступа к полям по индексу. Если вы добавите order=True в декоратор @dataclass(), ваши экземпляры станут полностью управляемыми, как (именованные) кортежи:

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, order=True)
class Base(DataclassSequence):
    x: int
    y: int

MyPy скоро будет явно поддерживать dataclasses; в версии 0.600 вы по-прежнему будете получать ошибки, поскольку он не распознает импорт модуля dataclasses или генерируется метод __new__.

В Python 3.6 и более ранних версиях вы также можете установить проект attrs для достижения того же эффекта; приведенный выше базовый класс последовательности выглядит следующим образом, используя 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 напрямую основан на attrs, а attrs предоставляет больше функциональных возможностей; mypy полностью поддерживает классы, созданные с помощью attrs.

Ответ 2

Существует PEP 544, в котором предлагается расширение типа системы, которое позволит использовать структурный подтипирование (статическая утиная печать). В скором времени будет улучшена реализация typing.NamedTuple во время выполнения, возможно, в Python 3.6.2 в конце июня (это будет также передано через typing в PyPI).