Достижение множественного наследования с использованием python классов данных - PullRequest
1 голос
/ 30 января 2020

Я пытаюсь использовать новые классы данных python, чтобы создать некоторые смешанные классы (уже сейчас, когда я пишу это, я думаю, это звучит как идея ra sh), и у меня возникли некоторые проблемы. Вот пример, приведенный ниже:

из классов данных, импортируемых из класса данных

@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__")

Если я тогда попробую:

nandn = NamedAndNumbered('n_and_n')
print(nandn.name)
print(nandn.number)

Я получу

NumberedObj __post_init__
NamedAndNumbered __post_init__
n_and_n
1

Предложение он выполнил __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__")

Но это просто дает мне ошибку AttributeError: 'super' object has no attribute '__post_init__', когда я пытаюсь вызвать NamedObj.__post_init__().

На данный момент я не совсем уверен, является ли это ошибкой / особенностью классов данных или чем-то, что связано с моим, вероятно, ошибочным пониманием подхода Python к наследованию. Кто-нибудь может протянуть руку?

Ответы [ 2 ]

2 голосов
/ 30 января 2020

Это:

def __post_init__(self):
    super(NamedObj, self).__post_init__()
    super(NumberedObj, self).__post_init__()
    print("NamedAndNumbered __post_init__")

не делает то, что вы думаете, что делает. super(cls, obj) вернет прокси классу после cls в type(obj).__mro__ - так, в вашем случае, до object. И весь смысл кооперативных super() звонков состоит в том, чтобы избежать необходимости явного звонка каждому из родителей.

Способ, которым кооперативные super() звонки предназначены для работы, это, ну, в общем, быть "кооперативным" - IOW все в mro должны передавать вызов следующему классу (на самом деле имя super довольно печальный выбор, поскольку речь идет не о вызове «суперкласса», а о «вызове следующего класса в mro»). «).

Итак, вы хотите, чтобы каждый из ваших «компонуемых» классов данных (которые не являются миксинами - только миксины имеют поведение) передавал вызов, поэтому вы можете создавать их в любом порядке. Первая наивная реализация будет выглядеть так:

@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__")

НО это не работает, поскольку для последнего класса в mro (здесь NamedObj) следующим классом в mro является встроенный object класс, который не имеет __post_init__ метода. Решение простое: просто добавьте базовый класс, который определяет этот метод как oop, и сделайте так, чтобы все ваши составные классы данных наследовали от него:

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__")
2 голосов
/ 30 января 2020

Проблема (скорее всего) не связана с dataclass es. Проблема в Python разрешении метода . Вызов метода для super() вызывает первый найденный метод из родительского класса в цепочке MRO . Поэтому, чтобы это работало, вам нужно вызвать методы родительских классов вручную:

@dataclass
class NamedAndNumbered(NumberedObj, NamedObj):

    def __post_init__(self):
        NamedObj.__post_init__(self)
        NumberedObj.__post_init__(self)
        print("NamedAndNumbered __post_init__")

Другой подход (если вам действительно нравится super()) может состоять в том, чтобы продолжить цепочку MRO, вызывая super() во всех родительские классы (но в цепочке должно быть __post_init__):

@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__")

В обоих подходах:

>>> 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
...