直接从object 继承的类是否应该调用super().__init__()?
您似乎在搜索这个问题的简单“是或否”答案,但不幸的是答案是“视情况而定”。此外,在决定是否应该调用super().__init__() 时,一个类是否直接继承自object 在某种程度上是无关紧要的。不变的是,如果调用object.__init__,则应该不带参数调用它——因为object.__init__ 不接受参数。
实际上,在协作继承情况下,这意味着您必须确保在调用object.__init__ 之前使用所有参数。它确实不意味着你应该尽量避免object.__init__被调用。 Here 是在调用 super 之前使用 args 的示例,response 和 request 上下文已从可变映射 kwargs 中弹出。
我之前提到过,一个类是否直接从object 继承是一个红鲱鱼1。但是我还没有提到应该激发这个设计决策的原因:如果您希望 MRO 继续搜索其他初始化程序 [阅读:其他 anymethods],则应该调用 super init [阅读:super anymethod]。如果您想指示应在此处停止 MRO 搜索,则不应调用 super。
如果object.__init__ 什么都不做,为什么它还存在呢?因为它确实做了一些事情:确保它在没有参数的情况下被调用。参数的存在可能表明存在错误2。 object 也用于停止超级调用链 - somebody 必须不调用 super,否则我们将无限递归。您可以更早地通过不调用 super 来明确地停止它。如果你不这样做,object 将作为最后一环,为你停止链条。
类 MRO 是在编译时确定的,通常是在定义类时/导入模块时。但是,请注意super 的使用涉及许多runtime 分支的机会。您必须考虑:
-
super 的方法使用哪些参数调用(即,您希望沿 MRO 转发哪些参数)
- 无论是否调用 super(有时您想故意破坏链条)
-
super本身是用哪些参数(如果有的话)创建的(下面描述了一个高级用例)
- 是否在您自己方法中的代码之前或之后调用代理方法(如果需要访问代理方法设置的某些状态,请先放置super,放置super最后,如果您要设置代理方法所依赖的某种状态 - 在某些情况下,您甚至希望将 super 放在中间某处!)
- 问题主要是关于
__init__,但不要忘记 super 也可以与任何其他方法一起使用
在极少数情况下,您可能会有条件地调用super。您可能会检查您的 super() 实例是否具有这个或那个属性,并围绕结果建立一些逻辑。或者,您可以调用super(OtherClass, self) 显式“跳过”链接并手动遍历此部分的 MRO。是的,如果默认行为不是您想要的,您可以劫持 MRO!所有这些恶魔般的想法的共同点是对C3 linearization算法的理解,Python如何制作MRO,以及super本身如何使用MRO。 Python 的实现或多或少源自another programming language,其中super 被命名为next-method。老实说,super 在 Python 中是一个非常糟糕的名称,因为它在初学者中造成了一个普遍的误解,即您总是“向上”调用其中一个父类,我希望他们选择了一个更好的名称。
在定义继承层次结构时,解释器无法知道您是想重用一些其他类现有的功能还是替换替代实现,或其他.任一决定都可能是有效且实用的设计。如果有一个关于何时以及如何调用super 的硬性规则,它不会留给程序员选择 - 语言将让你做出决定并自动做正确的事情。我希望这足以说明在 __init__ 中调用 super 并不是一个简单的是/否问题。
如果是,您将如何正确初始化SuperFoo?
(问题的this revision中Foo、SuperFoo等的来源)
为了回答这一部分,我假设 MCVE 中显示的 __init__ 方法实际上需要进行一些初始化(也许您可以在问题的 MCVE 代码中添加占位符 cmets 来达到此目的)。根本不要定义__init__,如果你唯一要做的就是用相同的参数调用super,那就没有意义了。不要将__init__ 定义为pass,除非您有意在此处停止MRO 遍历(在这种情况下,肯定需要添加注释!)。
首先,在我们讨论SuperFoo 之前,让我说NoSuperFoo 看起来像是一个不完整或糟糕的设计。如何将foo 参数传递给Foo 的init? 3 的 foo 初始值是硬编码的。硬编码(或以其他方式自动确定)foo 的初始值可能是可以的,但随后 you should probably be doing composition not inheritance。
对于SuperFoo,它继承了SuperCls和Foo。 SuperCls 看起来是为了继承,Foo 不是。这意味着您可能有一些工作要做,正如super harmful 中所指出的那样。一种前进的方式,as discussed in Raymond's blog,是编写适配器。
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
注意FooAdapter 有一个 Foo,而不是FooAdapter 是一个 Foo。这不是唯一可能的设计选择。但是,如果您像class FooParent(Foo) 一样继承,那么您就是在暗示FooParent 是 Foo,并且可以在Foo 本来是的任何地方使用 - 它是通过使用组合,通常更容易避免违反LSP。 SuperCls 也应该通过允许 **kwargs 合作:
class SuperCls:
def __init__(self, **kwargs):
# some other init code here
super().__init__(**kwargs)
也许SuperCls 也是你无法控制的,你也必须适应它,就这样吧。关键是,这是一种重用代码的方法,通过调整接口使签名匹配。假设每个人都很好地合作并消费他们需要的东西,最终super().__init__(**kwargs) 将代理到object.__init__(**{})。
由于我见过的 99% 的类在其构造函数中没有使用 **kwargs,这是否意味着 99% 的 python 类实现不正确?
不,因为YAGNI。 99% 的类是否需要立即支持 100% 的通用依赖注入以及所有的花里胡哨,然后才能有用?如果他们不这样做,他们会坏吗?例如,考虑集合文档中给出的OrderedCounter 配方。 Counter.__init__ 接受 *args 和 **kwargs,但接受 doesn't proxy them in the super init call。如果您想使用其中一个参数,那么运气不好,您必须覆盖 __init__ 并拦截它们。 OrderedDict 根本没有合作定义,实际上,一些 parent calls are hardcoded 到 dict - 并且不会调用任何下一行的 __init__,因此任何 MRO 遍历都会在那里停止。如果您不小心将其定义为 OrderedCounter(OrderedDict, Counter) 而不是 OrderedCounter(Counter, OrderedDict),元类基础仍然能够创建一致的 MRO,但该类根本无法用作有序计数器。
尽管存在所有这些缺点,@987654412@ 配方仍能像宣传的那样工作,因为 MRO 是按照为预期用例设计的而遍历的。因此,您甚至不需要 100% 正确地进行协作继承来实现依赖注入。这个故事的寓意是完美是进步的敌人(或者,practicality beats purity)。如果您想将MyWhateverClass 塞进任何您可以梦想的疯狂继承树中,请继续,但您需要编写必要的脚手架来实现这一点。像往常一样,Python 不会阻止你以任何足够好的方式来实现它。
1无论你是否在类声明中编写它,你总是从对象继承。为了与 2.7 运行时交叉兼容,许多开源代码库无论如何都会显式继承自对象。
2CPython 源代码here 中更详细地解释了这一点,以及__new__ 和__init__ 之间的微妙关系。