我目前正在尝试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

我怎么解决这个问题?


当前回答

下面的方法在使用纯python数据类和没有太多样板代码的情况下处理这个问题。

丑陋的:数据类。InitVar[bool]只是作为一个伪字段来帮助我们进行初始化,一旦创建实例就会丢失。而_ugly: bool = field(init=False)是一个实例成员,它不会通过__init__方法初始化,但也可以使用__post_init__方法初始化(你可以在这里找到更多)。

from dataclasses import dataclass, field, InitVar

@dataclass
class Parent:
    name: str
    age: int
    ugly: InitVar[bool]
    _ugly: bool = field(init=False)

    def __post_init__(self, ugly: bool):
        self._ugly = ugly

    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

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

jack.print_id()
jack_son.print_id()

注意,这使得字段ugly成为强制性的,使其成为可选的。你可以在父类上定义一个类方法,其中包含ugly作为可选参数:

from dataclasses import dataclass, field, InitVar

@dataclass
class Parent:
    name: str
    age: int
    ugly: InitVar[bool]
    _ugly: bool = field(init=False)

    def __post_init__(self, ugly: bool):
        self._ugly = ugly
    
    @classmethod
    def create(cls, ugly=True, **kwargs):
        return cls(ugly=ugly, **kwargs)

    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

jack = Parent.create(name='jack snr', age=32, ugly=False)
jack_son = Child.create(name='jack jnr', age=12, school='harvard')

jack.print_id()
jack_son.print_id()

现在您可以使用create(…)类方法作为创建父/子类的工厂方法,并使用默认值ugly。注意,这种方法必须使用命名参数才能工作。

其他回答

补充使用attrs的Martijn Pieters解决方案:可以在没有默认属性复制的情况下创建继承,使用:

import attr

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


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

关于kw_only参数的更多信息可以在这里找到

一个实验性但有趣的解决方案是使用元类。下面的解决方案允许使用带有简单继承的Python数据类,而完全不使用数据类装饰器。此外,它可以继承父基类的字段,而不必抱怨位置参数的顺序(非默认字段)。

from collections import OrderedDict
import typing as ty
import dataclasses
from itertools import takewhile

class DataClassTerm:
    def __new__(cls, *args, **kwargs):
        return super().__new__(cls)

class DataClassMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        fields = {}

        # Get list of base classes including the class to be produced(initialized without its original base classes as those have already become dataclasses)
        bases_and_self = [dataclasses.dataclass(super().__new__(cls, clsname, (DataClassTerm,), clsdict))] + list(bases)

        # Whatever is a subclass of DataClassTerm will become a DataClassTerm. 
        # Following block will iterate and create individual dataclasses and collect their fields
        for base in bases_and_self[::-1]: # Ensure that last fields in last base is prioritized
            if issubclass(base, DataClassTerm):
                to_dc_bases = list(takewhile(lambda c: c is not DataClassTerm, base.__mro__))
                for dc_base in to_dc_bases[::-1]: # Ensure that last fields in last base in MRO is prioritized(same as in dataclasses)
                    if dataclasses.is_dataclass(dc_base):
                        valid_dc = dc_base
                    else:
                        valid_dc = dataclasses.dataclass(dc_base)
                    for field in dataclasses.fields(valid_dc):
                        fields[field.name] = (field.name, field.type, field)
        
        # Following block will reorder the fields so that fields without default values are first in order
        reordered_fields = OrderedDict()
        for n, t, f  in fields.values():
            if f.default is dataclasses.MISSING and f.default_factory is dataclasses.MISSING:
                reordered_fields[n] = (n, t, f)
        for n, t, f  in fields.values():
            if n not in reordered_fields.keys():
                reordered_fields[n] = (n, t, f)
        
        # Create a new dataclass using `dataclasses.make_dataclass`, which ultimately calls type.__new__, which is the same as super().__new__ in our case
        fields = list(reordered_fields.values())
        full_dc = dataclasses.make_dataclass(cls_name=clsname, fields=fields, init=True, bases=(DataClassTerm,))
        
        # Discard the created dataclass class and create new one using super but preserve the dataclass specific namespace.
        return super().__new__(cls, clsname, bases, {**full_dc.__dict__,**clsdict})
    
class DataClassCustom(DataClassTerm, metaclass=DataClassMeta):
    def __new__(cls, *args, **kwargs):
        if len(args)>0:
            raise RuntimeError("Do not use positional arguments for initialization.")
        return super().__new__(cls, *args, **kwargs)

现在让我们创建一个带有父数据类和混合类的样本数据类:

class DataClassCustomA(DataClassCustom):
    field_A_1: int = dataclasses.field()
    field_A_2: ty.AnyStr = dataclasses.field(default=None)

class SomeOtherClass:
    def methodA(self):
        print('print from SomeOtherClass().methodA')

class DataClassCustomB(DataClassCustomA,SomeOtherClass):
    field_B_1: int = dataclasses.field()
    field_B_2: ty.Dict = dataclasses.field(default_factory=dict)

结果是

result_b = DataClassCustomB(field_A_1=1, field_B_1=2)

result_b
# DataClassCustomB(field_A_1=1, field_B_1=2, field_A_2=None, field_B_2={})

result_b.methodA()
# print from SomeOtherClass().methodA

尝试在每个父类上使用@dataclass装饰器做同样的事情会在接下来的子类中引发一个异常,如TypeError(f'non-default argument <field-name)跟随默认参数')。上面的解决方案防止了这种情况的发生,因为字段首先被重新排序。然而,由于字段的顺序被修改了,在DataClassCustom中防止*args的使用。__new__是强制的,因为原来的顺序不再有效。

虽然在Python >=3.10中引入了kw_only特性,本质上使数据类中的继承更加可靠,但上面的示例仍然可以用作一种使数据类可继承的方法,而不需要使用@dataclass装饰器。

一个快速而肮脏的解决方案:

from typing import Optional

@dataclass
class Child(Parent):
    school: Optional[str] = None
    ugly: bool = True

    def __post_init__(self):
        assert self.school is not None

然后返回并重构一次(希望如此)扩展了语言。

如果将属性从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

在发现数据类可能会获得一个允许字段重新排序的装饰器参数后,我回到了这个问题。这无疑是一个有希望的发展,尽管这一功能的进展似乎有些停滞。

现在,您可以通过使用dataclassy(我对数据类的重新实现,克服了这种挫折)来获得这种行为,以及其他一些细节。在原始示例中使用from dataclassy来代替from dataclassy意味着它运行时没有错误。

使用inspect打印Child的签名使正在发生的事情变得清晰;结果是(name: str, age: int, school: str, ugly: bool = True)。字段总是重新排序,以便在初始化式的参数中,具有默认值的字段位于不具有默认值的字段之后。两个列表(没有默认值的字段和有默认值的字段)仍然按照定义顺序排序。

面对这个问题是促使我编写数据类替代品的因素之一。这里详细介绍的变通方法虽然很有用,但要求将代码扭曲到完全否定数据类的简单方法(即字段顺序可以简单地预测)所提供的可读性优势的程度。