使用python数据类实现多重继承

Chr*_*per 4 python multiple-inheritance mixins method-resolution-order python-dataclasses

我正在尝试使用新的 python 数据类来创建一些混合类(在我写这篇文章时,我认为这听起来像是一个轻率的想法),但我遇到了一些问题。看下面的例子:

从数据类导入数据类

@dataclass
class NamedObj:
    name: str

    def __post_init__(self):
        print("NamedObj __post_init__")
        self.name = "Name: " + self.name

@dataclass
class NumberedObj:
    number: int = 0

    def __post_init__(self):
        print("NumberedObj __post_init__")
        self.number += 1

@dataclass
class NamedAndNumbered(NumberedObj, NamedObj):

    def __post_init__(self):
        super().__post_init__()
        print("NamedAndNumbered __post_init__")
Run Code Online (Sandbox Code Playgroud)

如果我再尝试:

nandn = NamedAndNumbered('n_and_n')
print(nandn.name)
print(nandn.number)
Run Code Online (Sandbox Code Playgroud)

我得到

NumberedObj __post_init__
NamedAndNumbered __post_init__
n_and_n
1
Run Code Online (Sandbox Code Playgroud)

暗示它已经运行__post_init__NamedObj,但没有运行NumberedObj。我想要的是让 NamedAndNumbered__post_init__为它的两个混合类 Named 和 Numbered 运行。有人可能认为如果NamedAndNumbered有这样的就可以做到__post_init__

def __post_init__(self):
    super(NamedObj, self).__post_init__()
    super(NumberedObj, self).__post_init__()
    print("NamedAndNumbered __post_init__")
Run Code Online (Sandbox Code Playgroud)

但是AttributeError: 'super' object has no attribute '__post_init__'当我尝试调用NamedObj.__post_init__().

在这一点上,我不完全确定这是数据类的错误/功能还是与我对 Python 继承方法的可能有缺陷的理解有关。有人可以伸出援手吗?

Yev*_*ych 6

该问题(很可能)与 es 无关dataclass。问题出在Python的方法解析上。调用方法 on会调用MROsuper()链中父类中第一个找到的方法。因此,要使其工作,您需要手动调用父类的方法:

@dataclass
class NamedAndNumbered(NumberedObj, NamedObj):

    def __post_init__(self):
        NamedObj.__post_init__(self)
        NumberedObj.__post_init__(self)
        print("NamedAndNumbered __post_init__")
Run Code Online (Sandbox Code Playgroud)

另一种方法(如果你真的喜欢super())可能是通过调用super()所有父类来继续 MRO 链(但它需要有一个__post_init__in 链):

@dataclass
class MixinObj:
    def __post_init__(self):
        pass

@dataclass
class NamedObj(MixinObj):
    name: str

    def __post_init__(self):
        super().__post_init__()
        print("NamedObj __post_init__")
        self.name = "Name: " + self.name

@dataclass
class NumberedObj(MixinObj):
    number: int = 0

    def __post_init__(self):
        super().__post_init__()
        print("NumberedObj __post_init__")
        self.number += 1

@dataclass
class NamedAndNumbered(NumberedObj, NamedObj):

    def __post_init__(self):
        super().__post_init__()
        print("NamedAndNumbered __post_init__")
Run Code Online (Sandbox Code Playgroud)

在这两种方法中:

>>> nandn = NamedAndNumbered('n_and_n')
NamedObj __post_init__
NumberedObj __post_init__
NamedAndNumbered __post_init__
>>> print(nandn.name)
Name: n_and_n
>>> print(nandn.number)
1
Run Code Online (Sandbox Code Playgroud)


bru*_*ers 6

这个:

def __post_init__(self):
    super(NamedObj, self).__post_init__()
    super(NumberedObj, self).__post_init__()
    print("NamedAndNumbered __post_init__")
Run Code Online (Sandbox Code Playgroud)

不会做你认为它会做的事情。super(cls, obj) clsin之后返回一个类的代理type(obj).__mro__- 所以,在你的情况下,到object. 合作super()调用的全部意义在于避免必须明确地调用每个父母。

合作super()调用的工作方式是,通过“合作” - IOW,mro 中的每个人都应该将调用转发给下一个班级(实际上,这个super名字是一个相当可悲的选择,因为它不是关于调用“超级班级”,而是关于“在 mro 中调用下一个班级”)。

IOW,您希望您的每个“可组合”数据类(不是 mixins - mixins 只有行为)来中继调用,因此您可以按任何顺序组合它们。第一个简单的实现看起来像:

@dataclass
class NamedObj:
    name: str

    def __post_init__(self):
        super().__post_init__()
        print("NamedObj __post_init__")
        self.name = "Name: " + self.name

@dataclass
class NumberedObj:
    number: int = 0

    def __post_init__(self):
        super().__post_init__()
        print("NumberedObj __post_init__")
        self.number += 1

@dataclass
class NamedAndNumbered(NumberedObj, NamedObj):

    def __post_init__(self):
        super().__post_init__()
        print("NamedAndNumbered __post_init__")
Run Code Online (Sandbox Code Playgroud)

但这不起作用,因为对于 mro 中的最后一个类(此处NamedObj),mro 中的下一个类是内置object类,它没有__post_init__方法。解决方案很简单:只需添加一个将这个方法定义为 noop 的基类,并使所有可组合的数据类都继承自它:

class Base(object):
    def __post_init__(self):
        # just intercept the __post_init__ calls so they
        # aren't relayed to `object`
        pass

@dataclass
class NamedObj(Base):
    name: str

    def __post_init__(self):
        super().__post_init__()
        print("NamedObj __post_init__")
        self.name = "Name: " + self.name

@dataclass
class NumberedObj:
    number: int = 0

    def __post_init__(self):
        super().__post_init__()
        print("NumberedObj __post_init__")
        self.number += 1

@dataclass
class NamedAndNumbered(NumberedObj, NamedObj):

    def __post_init__(self):
        super().__post_init__()
        print("NamedAndNumbered __post_init__")
Run Code Online (Sandbox Code Playgroud)