Python 3.7 数据类中的类继承

新手上路,请多包涵

我目前正在尝试 Python 3.7 中引入的新数据类结构。我目前坚持尝试对父类进行一些继承。看起来参数的顺序被我当前的方法搞砸了,以至于子类中的 bool 参数在其他参数之前传递。这会导致类型错误。

 from dataclasses import dataclass

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = False

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f'The Name is {self.name} and {self.name} is {self.age} year old')

@dataclass
class Child(Parent):
    school: str
    ugly: bool = True

jack = Parent('jack snr', 32, ugly=True)
jack_son = Child('jack jnr', 12, school = 'havard', ugly=True)

jack.print_id()
jack_son.print_id()

当我运行这段代码时,我得到这个 TypeError

 TypeError: non-default argument 'school' follows default argument

我该如何解决?

原文由 Mysterio 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.1k
2 个回答

数据类组合属性的方式使您无法在基类中使用具有默认值的属性,然后在子类中使用没有默认值的属性(位置属性)。

这是因为属性是通过从 MRO 的底部开始组合的,并按照先见顺序构建属性的有序列表;覆盖保留在其原始位置。 So Parent starts out with ['name', 'age', 'ugly'] , where ugly has a default, and then Child adds ['school'] to the该列表的末尾( ugly 已经在列表中)。这意味着您最终得到 ['name', 'age', 'ugly', 'school'] 并且因为 school 没有默认值,这导致 __init__ 的参数列表无效

这在 继承 下的 PEP-557 Dataclasses 中有记录:

当数据类由 @dataclass 装饰器创建时,它会在反向 MRO 中查看该类的所有基类(即,从 object 开始),并且对于每个数据类它找到的,将该基类中的字段添加到字段的有序映射中。添加所有基类字段后,它会将自己的字段添加到有序映射中。所有生成的方法都将使用这种组合的、计算的有序字段映射。因为字段是按插入顺序排列的,所以派生类会覆盖基类。

并根据 规格

TypeError 如果没有默认值的字段跟随有默认值的字段,将引发。当这发生在单个类中或作为类继承的结果时,情况就是如此。

您在这里有几个选项可以避免这个问题。

第一个选项是使用单独的基类来强制将具有默认值的字段置于 MRO 顺序中的较晚位置。无论如何,避免直接在要用作基类的类上设置字段,例如 Parent

以下类层次结构有效:

 # base classes with fields; fields without defaults separate from fields with.
@dataclass
class _ParentBase:
    name: str
    age: int

@dataclass
class _ParentDefaultsBase:
    ugly: bool = False

@dataclass
class _ChildBase(_ParentBase):
    school: str

@dataclass
class _ChildDefaultsBase(_ParentDefaultsBase):
    ugly: bool = True

# public classes, deriving from base-with, base-without field classes
# subclasses of public classes should put the public base class up front.

@dataclass
class Parent(_ParentDefaultsBase, _ParentBase):
    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f"The Name is {self.name} and {self.name} is {self.age} year old")

@dataclass
class Child(_ChildDefaultsBase, Parent, _ChildBase):
    pass

通过将字段拉出到 单独 的基类中,这些基类具有没有默认值的字段和具有默认值的字段,以及精心选择的继承顺序,您可以生成一个 MRO,将所有没有默认值的字段放在具有默认值的字段之前。 — 的反向 MRO(忽略 object Child 是:

 _ParentBase
_ChildBase
_ParentDefaultsBase
Parent
_ChildDefaultsBase

请注意,虽然 Parent 没有设置任何新字段,但它确实继承了 _ParentDefaultsBase 的字段,并且 不应 在字段列表顺序中以“最后”结束;上面的命令将 _ChildDefaultsBase 最后放置,因此它的字段“获胜”。数据类规则也得到满足; the classes with fields without defaults ( _ParentBase and _ChildBase ) precede the classes with fields with defaults ( _ParentDefaultsBase and _ChildDefaultsBase ).

The result is Parent and Child classes with a sane field older, while Child is still a subclass of Parent :

 >>> from inspect import signature
>>> signature(Parent)
<Signature (name: str, age: int, ugly: bool = False) -> None>
>>> signature(Child)
<Signature (name: str, age: int, school: str, ugly: bool = True) -> None>
>>> issubclass(Child, Parent)
True

所以你可以创建这两个类的实例:

 >>> jack = Parent('jack snr', 32, ugly=True)
>>> jack_son = Child('jack jnr', 12, school='havard', ugly=True)
>>> jack
Parent(name='jack snr', age=32, ugly=True)
>>> jack_son
Child(name='jack jnr', age=12, school='havard', ugly=True)

另一种选择是仅使用具有默认值的字段;您仍然可以通过在 __post_init__ 中提高一个值来犯错误以不提供 school 值:

 _no_default = object()

@dataclass
class Child(Parent):
    school: str = _no_default
    ugly: bool = True

    def __post_init__(self):
        if self.school is _no_default:
            raise TypeError("__init__ missing 1 required argument: 'school'")

但这 确实 改变了字段顺序; schoolugly 之后结束:

 <Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>

类型提示检查器 抱怨 _no_default 不是字符串。

您还可以使用 attrs 项目,该项目启发了 dataclasses 。它使用不同的继承合并策略; it pulls overridden fields in a subclass to the end of the fields list, so ['name', 'age', 'ugly'] in the Parent class becomes ['name', 'age', 'school', 'ugly'] in the Child 班级;通过使用默认值覆盖字段, attrs 允许覆盖而无需执行 MRO 舞蹈。

attrs 支持定义没有类型提示的字段,但让我们通过设置坚持 支持的类型提示模式 auto_attribs=True

 import attr

@attr.s(auto_attribs=True)
class Parent:
    name: str
    age: int
    ugly: bool = False

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f"The Name is {self.name} and {self.name} is {self.age} year old")

@attr.s(auto_attribs=True)
class Child(Parent):
    school: str
    ugly: bool = True

原文由 Martijn Pieters 发布,翻译遵循 CC BY-SA 4.0 许可协议

如果从 init 函数中排除它们,则可以在父类中使用具有默认值的属性。如果您需要在 init 时覆盖默认值的可能性,请使用 Praveen Kulkarni 的答案扩展代码。

 from dataclasses import dataclass, field

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(default=False, init=False)

@dataclass
class Child(Parent):
    school: str

jack = Parent('jack snr', 32)
jack_son = Child('jack jnr', 12, school = 'havard')
jack_son.ugly = True

甚至

@dataclass
class Child(Parent):
    school: str
    ugly = True
    # This does not work
    # ugly: bool = True

jack_son = Child('jack jnr', 12, school = 'havard')
assert jack_son.ugly

原文由 SimonMarcin 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题