当直接从`object`继承时,我应该调用super().__ init __()吗?

Ara*_*Fey 7 python inheritance super

因为这个问题是关于继承的super,所以我们先写一个类.这是一个代表一个人的简单日常课程:

class Person:
    def __init__(self, name):
        super().__init__()

        self.name = name
Run Code Online (Sandbox Code Playgroud)

像每个好类一样,它在初始化之前调用它的父构造函数.这个班级完美地完成了它的工作; 它可以毫无问题地使用:

>>> Person('Tom')
<__main__.Person object at 0x7f34eb54bf60>
Run Code Online (Sandbox Code Playgroud)

但是当我尝试创建一个继承自两个Person类和另一个类的类时,事情突然出现了:

class Horse:
    def __init__(self, fur_color):
        super().__init__()

        self.fur_color = fur_color

class Centaur(Person, Horse):
    def __init__(self, name, fur_color):
        # ??? now what?
        super().__init__(name)  # throws TypeError: __init__() missing 1 required positional argument: 'fur_color'
        Person.__init__(self, name)  # throws the same error
Run Code Online (Sandbox Code Playgroud)

由于钻石继承(object顶部的类),因此无法Centaur正确初始化实例.将super().__init__()Person结束了通话Horse.__init__,因为它抛出一个异常fur_color参数丢失.

但是如果PersonHorse不打电话,这个问题就不会存在super().__init__().

这引出了一个问题:类似于直接从object调用继承super().__init__()吗?如果是,您将如何正确初始化Centaur


免责声明:我知道什么super,该怎么MRO工作,以及如何super与多重继承交互.我明白是什么导致了这个错误.我只是不知道避免错误的正确方法是什么.


为什么我要具体询问,object即使钻石继承也可以与其他类一起发生?那是因为object在python的类型层次结构中有一个特殊的位置 - 无论你喜欢与否,它都位于你的MRO的顶部.通常钻石继承只有在您故意从某个基类继承以实现与该类相关的某个目标的目标时才会发生.在这种情况下,可以预期钻石继承.但是,如果钻石顶部的类是object,你的两个父类很可能是完全不相关的,并且有两个完全不同的接口,所以出错的可能性更大.

use*_*ica 8

如果Person并且Horse从未设计为用作同一类的基类,那么Centaur可能不应该存在.正确地设计多重继承非常困难,不仅仅是调用super.即使单一继承也相当棘手.

如果PersonHorse 应该支持这样的类创建Centaur,然后PersonHorse(可能他们周围的类)需要进行一些重新设计.这是一个开始:

class Person:
    def __init__(self, *, name, **kwargs):
        super().__init__(**kwargs)
        self.name = name

class Horse:
    def __init__(self, *, fur_color, **kwargs):
        super().__init__(**kwargs)
        self.fur_color = fur_color

class Centaur(Person, Horse):
    pass

stevehorse = Centaur(name="Steve", fur_color="brown")
Run Code Online (Sandbox Code Playgroud)

你会发现一些变化.我们来看看清单吧.

首先,__init__签名现在*位于中间.该*商标的唯一关键字参数的开头:namefur_color现在的关键字只.当多继承图中的不同类采用不同的参数时,几乎不可能使位置参数安全地工作,因此为了安全起见,我们需要按关键字参数.(如果多个类需要使用相同的构造函数参数,情况会有所不同.)

其次,__init__签名**kwargs现在都采取.这样就可以Person.__init__接受它不理解的关键字参数fur_color,并将它们传递到线下,直到它们到达任何类理解它们为止.到参数达到时object.__init__,object.__init__应该收空kwargs.

第三,Centaur不再有自己的__init__了.使用PersonHorse重新设计,它不需要__init__.继承__init__Person将与CentaurMRO 做正确的事,传递fur_colorHorse.__init__.


wim*_*wim 6

应该直接从object调用继承的类super().__init__()吗?

你好像在搜索这个问题的一个简单的"是或否"答案,但不幸的是答案是"它取决于".此外,在决定是否应该调用时super().__init__(),类是否直接继承是有点无关紧要的object.什么是不变的是,如果object.__init__被调用,它应该在没有参数的情况下调用 - 因为object.__init__它不接受参数.

实际上,在协作继承情况下,这意味着您必须确保在调用之前消耗所有参数object.__init__.它并不意味着你应该尽量避免object.__init__被调用.以下是在调用之前使用args的示例super,response并且request已经从可变映射中弹出了上下文kwargs.

我之前提到过,一个类是否直接继承object是一个红鲱鱼1.但是我还没有提到应该激发这个设计决定的内容:anymethod如果你想让MRO继续搜索其他初始化器,你应该调用super init [read:super ] [read:other anymethods].如果要指示应在此处停止MRO搜索,则不应调用super.

object.__init__如果它什么都不做的话,为什么存在呢?因为它确实做了一些事情:确保它不带参数调用.参数的存在可能表明存在错误2.object也有助于阻止超级电话链 - 有人不得打电话给超级,否则我们无限地进行.你可以通过不调用super来自己明确地停止它.如果您不这样做,object将作为最终链接并为您停止链接.

类MRO在编译时确定,通常在定义类时/导入模块时.但是,请注意,使用super涉及运行时分支的许多机会.你必须考虑:

  • 调用哪个方法的参数super(即你想沿着MRO转发哪些参数)
  • 是否完全调用super(有时你想故意破坏链)
  • 创建super 自身的参数(如果有的话)(有一个高级用例,如下所述)
  • 是否在您自己的方法中在代码之前之后调用代理方法(如果需要访问代理方法设置的某些状态,则先放置超级方法,如果您设置代理方法所依赖的某些状态,则将超级优先级放在最后已经 - 在某些情况下你甚至想把超级放在中间的某个地方!)
  • 问题主要是关于__init__,但不要忘记,super也可以与任何其他方法一起使用

在极少数情况下,您可以有条件地调用super呼叫.您可以检查您的super()实例是否具有此属性或该属性,并根据结果建立一些逻辑.或者,您可以调用super(OtherClass, self)显式"跳过"链接并手动遍历此部分的MRO.是的,如果默认行为不是你想要的,你可以劫持MRO!所有这些恶魔般的想法的共同点是对C3线性化算法的理解,Python如何制作MRO,以及超级本身如何使用MRO.Python的实现或多或少地取自另一种编程语言,其中super被命名next-method.老实说,super在Python中是一个超级糟糕的名字,因为它会在初学者中引起一个常见的误解,即你总是向其中一个父类调用"up",我希望他们选择了一个更好的名字.

在定义继承层次结构时,解释器无法知道您是否要重用其他类的现有功能,或者将其替换为替代实现或其他内容.任何一个决定都可能是一个有效和实用的设计.如果有一个关于何时以及如何super调用的硬性规则,则不会让程序员选择 - 语言会从你手中做出决定并自动做正确的事情.我希望能充分解释调用super in __init__不是一个简单的是/否问题.

如果是,您将如何正确初始化SuperFoo

(来源为Foo,SuperFoo在等这个版本的问题)

为了回答这一部分,我将假设__init__MCVE中显示的方法实际上需要进行一些初始化(也许你可以在问题的MCVE代码中添加占位符注释).__init__如果你所做的只是用相同的参数调用super,那么根本不要定义一个,没有意义.不要定义__init__那个只是pass,除非你故意意味着停止那里的MRO遍历(在这种情况下,评论肯定是正当的!).

首先,在我们讨论之前SuperFoo,让我说这NoSuperFoo看起来像一个不完整或糟糕的设计.你如何将foo参数传递给Foo初始化?的foo的初始化值3是硬编码.硬编码(或以其他方式自动确定)foo的init值可能没问题,但是你可能应该做的是组合而不是继承.

至于SuperFoo,它继承SuperClsFoo.SuperCls看起来是为了继承,Foo不是.这意味着你可能有一些工作要做,正如超级有害的指出.正如Raymond的博客中所讨论的,前进的一种方法是编写适配器.

class FooAdapter:
    def __init__(self, **kwargs):
        foo_arg = kwargs.pop('foo')  
        # can also use kwargs['foo'] if you want to leave the responsibility to remove 'foo' to someone else
        # can also use kwargs.pop('foo', 'foo-default') if you want to make this an optional argument
        # can also use kwargs.get('foo', 'foo-default') if you want both of the above
        self._the_foo_instance = Foo(foo_arg)
        super().__init__(**kwargs)

    # add any methods, wrappers, or attribute access you need

    @property
    def foo():
        # or however you choose to expose Foo functionality via the adapter
        return self._the_foo_instance.foo
Run Code Online (Sandbox Code Playgroud)

注意FooAdapter 有一个 Foo,不是FooAdapter 一个 Foo.这不是唯一可能的设计选择.但是,如果你继承了class FooParent(Foo),那么你暗示a FooParent 是a Foo,并且可以在任何其他地方Foo使用 - 通常使用组合避免违反LSP更容易. SuperCls还应该允许**kwargs:

class SuperCls:
    def __init__(self, **kwargs):
        # some other init code here
        super().__init__(**kwargs)
Run Code Online (Sandbox Code Playgroud)

也许SuperCls也是你无法控制的,你也必须适应它,所以就这样吧.关键是,这是一种重用代码的方法,通过调整接口使签名匹配.假设每个人都在合作并且消费他们需要的东西,最终super().__init__(**kwargs)将代理object.__init__(**{}).

由于我见过的99%的类都没有**kwargs在它们的构造函数中使用,这是否意味着99%的python类实现不正确?

不,因为YAGNI.在它们有用之前,99%的课程需要立即支持100%普遍依赖注入所有的花里胡哨吗?如果他们没有,他们会被打破吗?例如,考虑OrderedCounter集合文档中给出的配方.Counter.__init__接受*args**kwargs,但不会代理他们在超级初始化调用.如果你想使用其中一个参数,那么运气好的话,你必须覆盖__init__并拦截它们.OrderedDict根本没有合作定义,实际上,一些父调用是硬编码dict- 并且__init__没有调用下一行的任何内容,因此任何MRO遍历都会在那里停止.如果你不小心将它定义为OrderedCounter(OrderedDict, Counter)而不是OrderedCounter(Counter, OrderedDict)元类基础仍然能够创建一致的MRO,但该类根本不能作为有序计数器工作.

尽管存在所有这些缺点,但OrderedCounter配方的工作方式与广告一样,因为MRO是按照预期的用例设计的.因此,您甚至不需要100%正确地进行协作继承以实现依赖注入.故事的寓意是,完美是进步的敌人(或者,实用性胜过纯洁).如果你想填充MyWhateverClass任何疯狂的继承树,你可以梦想,继续,但你需要编写必要的脚手架来实现这一目标.像往常一样,Python不会阻止你以任何hacky方式实现它,并且足够好用.

1 无论是否在类声明中编写它,您总是继承自object.许多开源代码库无论如何都会明确地从对象继承,以便与2.7运行时交叉兼容.

2 这一点更详细地解释的那样,与之间的微妙关系沿着__new____init__,在CPython的来源这里.