【问题标题】:A way to subclass NamedTuple for purposes of typechecking一种将 NamedTuple 子类化以进行类型检查的方法
【发布时间】: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 bugNamedTuple 继承实现中。

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

我应该放弃这个想法吗?

【问题讨论】:

    标签: python type-hinting mypy python-typing namedtuple


    【解决方案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。
    • 实际上是故意放弃运行时更改。已决定命名元组应保持简约,并且对于所有复杂的用例,用户都应使用数据类。
    猜你喜欢
    • 2020-12-23
    • 2016-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-15
    • 2017-06-19
    • 1970-01-01
    相关资源
    最近更新 更多