一种为类型检查目的子类化 NamedTuple 的方法

A way to subclass NamedTuple for purposes of typechecking

我有几个共享某些字段的 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: error: Argument 1 to "DoSomething" has incompatible type "BaseExtended"; expected "Base"

有没有办法构建我的代码并保持 mypy 类型检查?我无法从 Base 继承 BaseExtended,因为 NamedTuple 继承实现中的 there's a bug

我也不想使用丑陋的 Union[Base, BaseExtended],因为当我尝试对 List 进行类型检查时这会中断,因为 List[Union[Base, BaseExtended]] 不等于 List[BaseExtended] 由于 some mypy magic about variant/covariant types.

我应该放弃这个想法吗?

PEP 544 提议对类型系统进行扩展,以允许结构子类型化(静态鸭子类型化)。 typing.NamedTuple 的运行时实现也将很快得到改进,可能在 6 月底的 Python 3.6.2 中(这也将通过 PyPI 上的 typing 向后移植)。

命名元组的构造方式使得从 typing.NamedTuple classes 继承尚不可能。您必须编写自己的 metaclass 来扩展 typing.NamedTupleMeta class 才能使 subclassing 工作,甚至 .

相反,您想使用新的 dataclasses module 来定义您的 classes 并实现继承:

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 the backport 在 Python 3.6 中使用。

上面定义了两个不可变的classes,具有xy属性,BaseExtendedclass又增加了一个属性。 BaseExtendedBase 的完整子 class,因此出于打字目的符合 DoSomething() 函数的要求。

classes 不是全名元组,因为它们没有长度或不支持索引,但是通过创建继承自 [=24= 的基 class 可以轻松添加], 添加两种方法通过索引访问字段。如果您将 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

MyPywill soon support dataclasses explicitly;在 0.600 版本中,您仍然会遇到错误,因为它无法识别 dataclasses 模块导入或生成了 __new__ 方法。

在Python 3.6及更早版本中,您也可以安装attrs project来达到同样的效果;上面的序列基础 class 使用 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 直接基于 attrsattrs 提供更多功能; mypy 完全支持使用 attrs.

生成的 classes