【问题标题】:If you store optional functionality of a base class in a secondary class, should the secondary class subclass the base class?如果将基类的可选功能存储在次要类中,次要类是否应该继承基类?
【发布时间】:2020-12-17 11:03:19
【问题描述】:

我知道标题可能有点混乱,所以让我举个例子。假设您有一个基类Base,它旨在被子类化以创建更复杂的对象。但是你也有可选的功能,你不需要每个子类,所以你把它放在一个二级类OptionalStuffA,它总是打算和基类一起子类化。你是否也应该让那个二级类成为Base的子类?

这当然只有当你有多个OptionalStuff 类并且你想以不同的方式组合它们时才相关,因为否则你不需要子类BaseOptionalStuffA (并且只需OptionalStuffABase 的子类,所以你只需要子类OptionalStuffA)。我知道如果 Base 不止一次继承自 MRO,这对 MRO 没有影响,但我不确定让所有次要类都继承自 Base 是否有任何缺点。

下面是一个示例场景。我还将QObject 类作为“第三方”令牌类添加,其功能对于辅助类之一的工作是必需的。我在哪里子类化它?下面的例子展示了我到目前为止是如何做到的,但我怀疑这是要走的路。

from PyQt5.QtCore import QObject

class Base:
    def __init__(self):
        self._basic_stuff = None

    def reset(self):
        self._basic_stuff = None


class OptionalStuffA:
    def __init__(self):
        super().__init__()
        self._optional_stuff_a = None

    def reset(self):
        if hasattr(super(), 'reset'):
            super().reset()
        self._optional_stuff_a = None

    def do_stuff_that_only_works_if_my_children_also_inherited_from_Base(self):
        self._basic_stuff = not None


class OptionalStuffB:
    def __init__(self):
        super().__init__()
        self._optional_stuff_b = None

    def reset(self):
        if hasattr(super(), 'reset'):
            super().reset()
        self._optional_stuff_b = None

    def do_stuff_that_only_works_if_my_children_also_inherited_from_QObject(self):
        print(self.objectName())


class ClassThatIsActuallyUsed(Base, OptionalStuffA, OptionalStuffB, QObject):
    def __init__(self):
        super().__init__()
        self._unique_stuff = None

    def reset(self):
        if hasattr(super(), 'reset'):
            super().reset()
        self._unique_stuff = None

【问题讨论】:

  • 对我来说,if hasattr(superdo_stuff_that 表示应该使用继承。
  • 这是一个很好的观点。我这样做是因为如果我只调用super().reset() 并到达ClassThatIsActuallyUsed 的MRO 的末尾,它会抛出一个错误,因为QObject 没有那个方法。
  • 子类化BaseQObject 的另一个优点当然是编辑器不会在二级类中标记未解析的引用。
  • OptionalStuff 这样提供额外功能的类被称为mixin 类,并且继承自object,因此它们可以在各种上下文中使用并避免当类D 继承自类CB(它们都是类A 的子类)时可能发生的钻石问题。简而言之,答案是“不”。
  • 我明白了,谢谢@Booboo!我在某个时候读过关于钻石问题的文章,但又忘记了。也许这就是我按照我的方式设置课程的原因。我现在正在修改我的代码,并认为必须有一种方法来改进它。我仍然不太喜欢我如何处理整个if hasattr(super(), 'reset') 的事情。有没有更好的方法来做到这一点并确保所有(祖)父类都调用reset?甚至按特定顺序?如果您需要在一个 oder 中调用一种方法而在不同的一种中调用另一种方法怎么办?因为只有一个 MRO,我猜这是不可能的?

标签: python multiple-inheritance method-resolution-order


【解决方案1】:

我可以从您的问题中得到的是,您希望根据不同的条件具有不同的功能和属性,这听起来是使用 MetaClass 的好理由。 这完全取决于您的每个类有多复杂,以及您要构建什么,如果它是针对某些库或 API 的,那么如果使用得当,MetaClass 可以发挥作用。

MetaClass 非常适合根据某种条件向类添加函数和属性,您只需将所有子类函数添加到一个元类中,然后将该 MetaClass 添加到您的主类中

从哪里开始

您可以阅读有关 MetaClass 的信息here,或者您可以观看它here。 在您对 MetaClass 有了更好的了解后,请查看来自 herehere 的 Django ModelForm 的源代码,但在此之前先简要了解一下 Django Form 如何从外部工作,这将使您了解如何实现它.

这就是我的实现方式。

#You can also inherit it from other MetaClass but type has to be top of inheritance
class meta_class(type):
    # create class based on condition

    """
    msc: meta_class, behaves much like self (not exactly sure).
    name: name of the new class (ClassThatIsActuallyUsed).
    base: base of the new class (Base).
    attrs: attrs of the new class (Meta,...).
    """

    def __new__(mcs, name, bases, attrs):
        meta = attrs.get('Meta')
        if(meta.optionA){
            attrs['reset'] = resetA
        }if(meta.optionB){
            attrs['reset'] = resetB
        }if(meta.optionC){
            attrs['reset'] = resetC
        }
        if("QObject" in bases){
            attrs['do_stuff_that_only_works_if_my_children_also_inherited_from_QObject'] = functionA
        }
        return type(name, bases, attrs)


class Base(metaclass=meta_class): #you can also pass kwargs to metaclass here

    #define some common functions here
    class Meta:
        # Set default values here for the class
        optionA = False
        optionB = False
        optionC = False


class ClassThatIsActuallyUsed(Base):
    class Meta:
        optionA = True
        # optionB is False by default
        optionC = True

编辑:详细说明如何实现 MetaClass。

【讨论】:

  • 感谢您的建议!我以前从未使用过元类,但我很高兴熟悉这个概念。我已经在我的帖子下的 cmets 中解释了我想具体使用这些类的内容。也许有了这些信息,您可以详细说明一下?
  • 这样的功能可以用___init_subclass__docs.python.org/3/reference/…更简单地实现
  • @VPfB 你能解释一下在这种情况下它是如何工作的吗?
  • @mapf 稍作修改meta_class.__new__ 将变为Base.__init_subclass__。不需要显式的元类代码。 __init_subclass__ 正是出于这个原因引入的:自定义类创建没有元类。请参阅 PEP487。如果你愿意,我可以用代码发布答案,但你的问题有点不同。
  • @VPfB 谢谢!是的,如果你能做到,那就太好了。
【解决方案2】:

让我从另一种选择开始。在下面的示例中,Base.foo 方法是一个普通的标识函数,但选项可以覆盖它。

class Base:
    def foo(self, x):
        return x

class OptionDouble:
    def foo(self, x): 
        x *= 2  # preprocess example
        return super().foo(x)

class OptionHex:
    def foo(self, x): 
        result = super().foo(x)
        return hex(result)  # postprocess example

class Combined(OptionDouble, OptionHex, Base):
    pass

b = Base()
print(b.foo(10)) # 10

c = Combined()
print(c.foo(10)) # 2x10 = 20, as hex string: "0x14"

关键是在Combined的定义中的bases是Options在Base之前指定的:

class Combined(OptionDouble, OptionHex, Base):

在这个简单的例子中,从左到右读取类名 这是foo() 实现的排序顺序。 它被称为方法解析顺序(MRO)。 它还定义了 super() 在特定类中的确切含义,这很重要,因为 Options 是作为 super() 实现的包装器编写的

反之则不行:

class Combined(Base, OptionDouble, OptionHex):
    pass

c = Combined()
print(Combined.__mro__)
print(c.foo(10))  # 10, options not effective!

在这种情况下,首先调用Base 实现并直接返回结果。

您可以手动处理正确的基本顺序,也可以编写一个检查它的函数。它遍历 MRO 列表,一旦看到 Base,它就不允许在其后出现 Option

class Base:
    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        base_seen = False
        for mr in cls.__mro__:
            if base_seen:
                if issubclass(mr, Option):
                    raise TypeError( f"The order of {cls.__name__} base classes is incorrect")
            elif mr is Base:
                base_seen = True

    def foo(self, x): 
        return x

class Option:
    pass

class OptionDouble(Option):
    ... 

class OptionHex(Option):
    ... 

现在回答您的评论。我写道@weettler 的方法可以简化。我的意思是这样的:

class Base:
    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        print("options for the class", cls.__name__)
        print('A', cls.optionA)
        print('B', cls.optionB)
        print('C', cls.optionC)
        # ... modify the class according to the options ...

        bases = cls.__bases__
        # ... check if QObject is present in bases ...

    # defaults
    optionA = False
    optionB = False
    optionC = False


class ClassThatIsActuallyUsed(Base):
    optionA = True
    optionC = True

此演示将打印:

options for the class ClassThatIsActuallyUsed
A True
B False
C True

【讨论】:

  • 非常感谢!不过,所有这些元类功能对我来说并不是超级直观,我需要一些时间来适应它。
  • 我从未研究过 init_subclass,因为在 MetaClass 出现时我已经习惯了使用它,但我喜欢这种方法。一件小事我可以检查在 ClassThatIsActuallyUsed 中继承了哪些其他类,如问题中的 QObject
猜你喜欢
  • 2021-11-08
  • 2019-07-30
  • 2017-07-18
  • 1970-01-01
  • 2016-09-02
  • 1970-01-01
  • 2013-09-26
  • 2020-10-09
  • 2021-12-21
相关资源
最近更新 更多