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

我怎么解决这个问题?


当前回答

您看到此错误是因为在具有默认值的实参之后添加了没有默认值的实参。继承字段到数据类中的插入顺序与方法解析顺序相反,这意味着父字段放在前面,即使它们稍后被它们的子字段覆盖。

来自PEP-557 -数据类的示例:

@dataclass 阶级基础: x: Any = 15.0 Y: int = 0 @dataclass C类(基础): Z: int = 10 X: int = 15 最终的字段列表是,按顺序,x, y, z。x的最终类型是int,在类C中指定。

不幸的是,我认为没有其他办法。我的理解是,如果父类有默认实参,那么子类就不能有非默认实参。

其他回答

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

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

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

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

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

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

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

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

如果你使用的是Python 3.10+,那么你可以在这个答案和Python文档中讨论的数据类中使用仅关键字参数。

如果你使用的是< Python 3.10,那么你可以利用数据类。字段,使用default_factory抛出。由于该属性将使用field()声明,因此它被视为具有默认值;但是,如果用户试图创建一个实例而没有提供该字段的值,它将使用工厂,这将会出错。

这种技术并不等同于仅使用关键字,因为您仍然可以按位置提供所有参数。但是,这确实解决了问题,而且比使用各种数据类dunder方法要简单得多。

from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, TypeVar

T = TypeVar("T")


def required() -> T:
    f: T

    def factory() -> T:
        # mypy treats a Field as a T, even though it has attributes like .name, .default, etc
        field_name = f.name  # type: ignore[attr-defined]
        raise ValueError(f"field '{field_name}' required")

    f = field(default_factory=factory)
    return f


@dataclass
class Event:
    id: str
    created_at: datetime
    updated_at: Optional[datetime] = None


@dataclass
class NamedEvent(Event):
    name: str = required()


event = NamedEvent(name="Some Event", id="ab13c1a", created_at=datetime.now())
print("created event:", event)


event2 = NamedEvent("ab13c1a", datetime.now(), name="Some Other Event")
print("created event:", event2)

event3 = NamedEvent("ab13c1a", datetime.now())

输出:

created event: NamedEvent(id='ab13c1a', created_at=datetime.datetime(2022, 7, 23, 19, 22, 17, 944550), updated_at=None, name='Some Event')
created event: NamedEvent(id='ab13c1a', created_at=datetime.datetime(2022, 7, 23, 19, 22, 17, 944588), updated_at=None, name='Some Other Event')
Traceback (most recent call last):
  File ".../gist.py", line 39, in <module>
    event3 = NamedEvent("ab13c1a", datetime.now())
  File "<string>", line 6, in __init__
  File ".../gist.py", line 14, in factory
    raise ValueError(f"field '{field_name}' required")
ValueError: field 'name' required

你也可以在github上找到这段代码。

当您使用Python继承创建数据类时,不能保证所有具有默认值的字段将出现在所有没有默认值的字段之后。

一个简单的解决方案是避免使用多重继承来构造“合并”数据类。相反,我们可以通过对父数据类的字段进行过滤和排序来构建合并的数据类。

试试这个merge_dataclasses()函数:

import dataclasses
import functools
from typing import Iterable, Type


def merge_dataclasses(
    cls_name: str,
    *,
    merge_from: Iterable[Type],
    **kwargs,
):
    """
    Construct a dataclass by merging the fields
    from an arbitrary number of dataclasses.

    Args:
        cls_name: The name of the constructed dataclass.

        merge_from: An iterable of dataclasses
            whose fields should be merged.

        **kwargs: Keyword arguments are passed to
            :py:func:`dataclasses.make_dataclass`.

    Returns:
        Returns a new dataclass
    """
    # Merge the fields from the dataclasses,
    # with field names from later dataclasses overwriting
    # any conflicting predecessor field names.
    each_base_fields = [d.__dataclass_fields__ for d in merge_from]
    merged_fields = functools.reduce(
        lambda x, y: {**x, **y}, each_base_fields
    )

    # We have to reorder all of the fields from all of the dataclasses
    # so that *all* of the fields without defaults appear
    # in the merged dataclass *before* all of the fields with defaults.
    fields_without_defaults = [
        (f.name, f.type, f)
        for f in merged_fields.values()
        if isinstance(f.default, dataclasses._MISSING_TYPE)
    ]
    fields_with_defaults = [
        (f.name, f.type, f)
        for f in merged_fields.values()
        if not isinstance(f.default, dataclasses._MISSING_TYPE)
    ]
    fields = [*fields_without_defaults, *fields_with_defaults]

    return dataclasses.make_dataclass(
        cls_name=cls_name,
        fields=fields,
        **kwargs,
    )

然后,您可以按照如下方式合并数据类。注意,我们可以合并A和B,默认字段B和d被移动到合并的数据类的末尾。

@dataclasses.dataclass
class A:
    a: int
    b: int = 0


@dataclasses.dataclass
class B:
    c: int
    d: int = 0


C = merge_dataclasses(
    "C",
    merge_from=[A, B],
)

# Note that 
print(C(a=1, d=1).__dict__)
# {'a': 1, 'd': 1, 'b': 0, 'c': 0}

当然,这种解决方案的缺陷是C实际上不继承A和B,这意味着您不能使用isinstance()或其他类型断言来验证C的亲本。