一种将 NamedTuple 子类化以进行类型检查的方法

Posted

技术标签:

【中文标题】一种将 NamedTuple 子类化以进行类型检查的方法【英文标题】:A way to subclass NamedTuple for purposes of typechecking 【发布时间】:2017-11-01 10:14:45 【问题描述】:

我有几个namedtuples 共享一些字段。我有一个接受这些元组的函数,并保证只与共享字段交互。我想在 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:错误:“DoSomething”的参数 1 具有不兼容的类型“BaseExtended”;预期的“基地”

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

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

我应该放弃这个想法吗?

【问题讨论】:

【参考方案1】:

命名元组的构造方式使得从typing.NamedTuple 类继承是不可能的。您必须编写自己的元类来扩展 typing.NamedTupleMeta 类以使子类化工作,即使这样 the class generated by collections.namedtuple() is just not built to extend。

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

from dataclasses import dataclass

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

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

该模块是 Python 3.7 中的新模块,但您可以在 Python 3.6 上pip install dataclasses the backport。

上面定义了两个具有xy 属性的不可变类,BaseExtended 类又增加了一个属性。 BaseExtendedBase 的完整子类,因此对于打字目的而言,它符合 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 will soon support dataclasses explicitly;在 0.600 版中,您仍然会收到错误,因为它无法识别 dataclasses 模块导入或生成 __new__ 方法。

在 Python 3.6 及更早的版本中,也可以安装attrs project 来达到同样的效果;上面的序列基类使用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 生成的类。

【讨论】:

我喜欢 NamedTuple 都有一个 _asdict(),所有数据类都有父类型吗?【参考方案2】:

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

【讨论】:

谢谢。我猜你不知道在 3.6.2 之前我可以使用任何优雅的解决方法吗?如果是这样,我会接受答案并继续前进。 @wuzwm 不,很遗憾我不知道。 改进尚未实现,同样的问题仍然适用于 Python 3.6.5 和 3.7.0b4。 实际上是故意放弃运行时更改。已决定命名元组应保持简约,并且对于所有复杂的用例,用户都应使用数据类。

以上是关于一种将 NamedTuple 子类化以进行类型检查的方法的主要内容,如果未能解决你的问题,请参考以下文章

如何子类化字典以支持泛型类型提示?

如何忽略类型检查并遵守 <80 个字符的行

namedtuple的简单使用

collections 模块(namedtuple, deque, Counter )

Python的namedtuple使用详解

无法为 namedtuple 的子类设置属性