为什么不会自动调用超类的 __init__ 方法?

186
为什么Python设计者决定子类的__init__()方法不会自动调用其超类的__init__()方法,与其他一些语言不同?Pythonic和推荐的惯用法是否真的像以下这样?
class Superclass(object):
    def __init__(self):
        print 'Do something'

class Subclass(Superclass):
    def __init__(self):
        super(Subclass, self).__init__()
        print 'Do something else'

2
你可以编写一个装饰器来继承__init__方法,甚至可以自动搜索子类并对其进行装饰。 - Sergey Orshanskiy
2
@osa,听起来是个非常好的想法。您能否再详细描述一下装饰器部分? - Diansheng
1
@osa 是的,请进一步说明! - Charlie Parker
@Diansheng 我猜这只是一个对 super() 的调用。 - user26742873
@CharlieParker 我已经添加了一篇关于此事的答案。 - user26742873
显示剩余3条评论
11个回答

185
Python的__init__与其他语言的构造函数之间的关键区别在于,__init__不是构造函数:它是一个初始化器(实际的构造函数(如果有的话,但请见下文;-)是__new__,并且工作方式完全不同)。虽然构造所有超类(毫无疑问,在继续向下构造之前这样做)显然是说您正在构造子类的实例的一部分,但对于初始化来说,情况显然并非如此,因为存在许多用例需要跳过,更改,控制超类的初始化--在子类初始化“中间”发生等。
基本上,Python中的超类初始化器委托不是自动完成的,原因与委托任何其他方法一样--请注意,“其他语言”也不会自动委托给任何其他方法...只是 构造函数(以及必要时解构函数),而正如我所提到的,这不是Python的__init__。 (__new__的行为也非常奇特,但实际上与您的问题没有直接关系,因为__new__是如此特殊的构造函数,它实际上不一定需要构造任何东西-- 完全可以返回现有实例,甚至是非实例...显然Python为您提供了比您所想的“其他语言”更多的控制机制,这也包括在__new__中没有自动委托!-)。

8
我被点赞是因为我认为真正的好处在于你提到的能够在子类初始化的任何时候(或者根本不初始化)调用超类的__init()__方法的能力。 - kindall
74
"-1" 表示 "__init__ 不是构造函数,实际的构造函数是 __new__"。正如你所提到的,__new__ 与其他语言中的构造函数完全不同。__init__ 实际上非常相似(它在创建新对象时调用,对象被分配后,为新对象设置成员变量),并且几乎总是实现在其他语言中应该放在构造函数中的功能的地方。因此,只需将其称为构造函数即可! - Ben
14
我认为这个说法相当荒谬:“构造所有超类……显然是在说你正在构造一个子类的实例,但对于初始化明显不是这种情况。” 在“构造/初始化”这些词中没有任何东西可以使任何人“显然”理解这一点。__new__也不会自动调用超类的__new__。因此,你声称“构造”必然涉及到构造超类,而“初始化”则不涉及的区别与你声称__new__是构造函数的主张不一致。 - Ben
6
在Python/Java术语中,__init__被称为构造函数。这个构造函数是一个初始化函数,在对象完全构建并初始化到其最终运行时类型的默认状态后被调用。它不等同于C++构造函数,后者会在静态类型已分配但未定义状态的对象上被调用。这些也与__new__不同,因此我们实际上有至少四种不同的分配/构造/初始化函数。不同的编程语言使用不同的术语,但重要的是行为而不是术语。 - Elazar
显示剩余5条评论

37

当人们像机械地引用“Python之禅”时,我有点尴尬,好像这是对任何事情的辩解。它是一种设计哲学;特定的设计决策总是可以用更具体的术语来解释--而且必须这样做,否则“Python之禅”就成为了为任何事情找借口。

原因很简单:在构造派生类时,不一定要以与构造基类完全相似的方式进行。您可能拥有更多或更少的参数,它们可能按不同的顺序排列或根本没有关系。

class myFile(object):
    def __init__(self, filename, mode):
        self.f = open(filename, mode)
class readFile(myFile):
    def __init__(self, filename):
        super(readFile, self).__init__(filename, "r")
class tempFile(myFile):
    def __init__(self, mode):
        super(tempFile, self).__init__("/tmp/file", mode)
class wordsFile(myFile):
    def __init__(self, language):
        super(wordsFile, self).__init__("/usr/share/dict/%s" % language, "r")

这适用于所有的派生方法,不仅仅是__init__


8
这个例子有什么特别之处吗?静态语言也可以做到这一点。 - jean
2
那么?它如何防止默认(无参数)超类构造函数被隐式调用?这就是其他语言所做的(C ++,Java等)。 - Géry Ogam

22

Java和C++要求调用基类构造函数,因为涉及内存布局。

如果您有一个带有成员field1的类BaseClass,并创建了一个添加了成员field2的新类SubClass,则SubClass的实例包含field1field2的空间。除非您要求所有继承类在自己的构造函数中重复BaseClass的初始化,否则需要BaseClass的构造函数来填充field1。如果field1是私有的,则继承类无法初始化field1

Python不是Java或C++。所有用户定义类的所有实例具有相同的“形状”。它们基本上只是字典,可以插入属性。在进行任何初始化之前,所有用户定义类的所有实例几乎完全相同;它们只是存储尚未存储任何属性的位置。

因此,Python子类不调用其基类构造函数是完全合理的。如果需要,它可以自己添加属性。在层次结构中为每个类预留一定数量的字段的空间,并且代码从BaseClass方法添加的属性与从SubClass方法添加的属性之间没有区别。

如果像通常一样,SubClass实际上想在继续自己的定制之前设置好所有BaseClass的不变量,那么是的,你可以调用BaseClass.__init__()(或使用super,但这有时很复杂并且会有它自己的问题)。但你不必这样做。你可以在之前、之后或使用不同的参数进行初始化。如果你愿意,你甚至可以完全从另一个方法中调用BaseClass.__init__; 也许你有一些奇怪的延迟初始化需要。

Python通过保持简单来实现这种灵活性。你通过编写一个设置self属性的__init__方法来初始化对象。就是这样。它的行为与方法完全相同,因为它确实是一个方法。没有其他奇怪和难以理解的规则,比如必须先做一些事情,否则会自动发生其他事情。它唯一需要服务的目的是作为钩子在对象初始化期间执行以设置初始属性值,并且它确实达到了这个目的。如果你想要它做其他事情,你需要在代码中显式地编写。


2
就C++而言,它与“内存布局”无关。C++可以实现与Python相同的初始化模型。C++中构造/析构之所以是这样的,唯一的原因是出于设计决策,为资源管理(RAII)提供可靠和良好的设施,这些设施也可以由编译器自动生成(意味着更少的代码和人为错误),因为它们的规则(调用顺序)已经严格定义。不确定,但Java很可能只是遵循了这种方法,成为另一个类似于C的语言。 - Alexander Shukaev
2
我认为这是一个糟糕的选择,但因为解释清晰而+1。 - Tony Delroy

18
为避免混淆,知道如果子类没有一个__init__()方法,你可以调用基类的__init__()方法是有用的。
例子:
class parent:
  def __init__(self, a=1, b=0):
    self.a = a
    self.b = b

class child(parent):
  def me(self):
    pass

p = child(5, 4)
q = child(7)
z= child()

print p.a # prints 5
print q.b # prints 0
print z.a # prints 1

事实上,在Python中,当子类找不到__init__()时,MRO会在父类中查找。如果您的子类已经有了__init__()方法,您需要直接调用父类构造函数。

例如,下面的代码将返回一个错误: class parent: def init(self, a=1, b=0): self.a = a self.b = b

    class child(parent):
      def __init__(self):
        pass
      def me(self):
        pass

    p = child(5, 4) # Error: constructor gets one argument 3 is provided.
    q = child(7)  # Error: constructor gets one argument 2 is provided.

    z= child()
    print z.a # Error: No attribute named as a can be found.

1
关键点在于[init]仅在实例化期间的继承树搜索中调用一次,且为最低层。如果存在超类[init]需要调用,则必须使用super().init()进行递归显式调用。如果沿着树路径没有定义[init],则不会调用任何内容。它只是一个普通函数,在构造时自动调用一次。您可以稍后使用类似child.init(p, 4, 5)的方式调用它。 - Leon Chang
1
这在Python文档中有记录吗?我找到了object.__init__()并阅读了“如果一个基类有一个__init__()方法,那么派生类的__init__()方法(如果有的话)必须显式调用它以确保正确初始化实例的基类部分”。但是这并没有说明当派生类缺少__init__()时的行为。 - ogdenkev
@ogdenkev 在你的引用中,“if any” 微妙地表明子类不必有一个 __init__ 方法。 - Jeyekomon
这个答案的价值超过了 MRO(https://www.python.org/download/releases/2.3/mro/)和 object.__init__() 的所有文档。顺便说一句,如果想要在子类中模拟一个 __init__() 而不是重载父类的 __init__,可以定义一个 main()(或者任何其他名称,不要紧)方法并从基类的 __init__() 中调用它,然后在子类中重载。来自子类的重载 main() 方法将在其调用父类 init 时运行。 - ChrisN

11

"显式优于隐式"。这个理论也表明我们应该显式地写出 'self'。

我认为最终这是有益的 - 你能够背诵所有关于调用超类构造函数的Java规则吗?


9
大部分情况下我同意你的观点,但Java的规则其实相当简单:除非你明确请求使用不同的构造函数,否则将调用无参构造函数。 - Laurence Gonsalves
2
@Laurence - 当父类没有定义无参构造函数时会发生什么?当无参构造函数是受保护的或私有的时会发生什么? - Mike Axiak
9
如果您尝试显式调用它,发生的事情与直接调用它一样。 - Laurence Gonsalves

8

现在,我们有一个相当长的页面,描述了多重继承情况下的方法解析顺序:http://www.python.org/download/releases/2.3/mro/

如果构造函数被自动调用,你需要另外一页至少同样长度的页面来解释发生的顺序。那将是一场噩梦...


这是正确的答案。Python需要定义子类和超类之间参数传递的语义。很遗憾这个答案没有得到赞同。也许如果它展示了一些例子来解释问题会更好? - Gary Weiss
这是一个答案,感谢提供链接,但在回答一个非常重要的问题时,用“你必须...”来描述假设情况并不是一个有效的推理。事实上,如果采用任何不同的方法来解释继承和实例化的顺序,那么由于当前实现中有太多的魔法,所以解释会变得非常困难,这表明实现是人为的,并且存在缺陷而不是被解决。 - ChrisN
@ChrisN 我想优雅的解决方案可能得等到Python 4了 :) - Jeremy Friesner

7
通常子类会有额外的参数,这些参数无法传递给超类。

那么呢?它如何防止默认(没有参数)的父类构造函数被隐式调用?这就是其他编程语言(C ++、Java等)所做的。 - Géry Ogam

3
也许__init__是子类需要覆盖的方法。有时,子类需要在添加特定于类的代码之前运行父函数,而其他时候需要在调用父函数之前设置实例变量。由于Python无法知道何时调用这些函数最合适,因此它不应该猜测。
如果这些还不能说服您,请考虑__init__只是另一个函数。如果所讨论的函数是dostuff,您是否仍希望Python自动调用父类中对应的函数?

2
我认为在这里非常重要的一个考虑因素是,通过自动调用super.__init__(),你可以设计调用初始化方法的时间和参数。避免自动调用它,并要求程序员显式地调用它,可以提供更多的灵活性。毕竟,仅仅因为类B是从类A派生出来的,并不意味着A.__init__()应该使用与B.__init__()相同的参数进行调用。显式调用意味着程序员可以完全不同的参数定义B.__init__(),使用这些数据进行一些计算,根据该方法的适当参数调用A.__init__(),然后进行一些后处理。如果A.__init__()会在B.__init__()隐式调用它之前或之后被调用,这种灵活性将很难实现。

1
正如Sergey Orshanskiy在评论中指出的那样,编写一个装饰器来继承__init__方法也很方便。

你可以编写一个装饰器来继承__init__方法,甚至可以自动搜索子类并对其进行装饰。 - Sergey Orshanskiy Jun 9 '15 at 23:17

第1部分/3:实现

注意:只有当你想要调用基类和派生类的__init__时,这才有用,因为__init__会自动继承。请参阅此问题的先前答案。

def default_init(func):
    def wrapper(self, *args, **kwargs) -> None:
        super(type(self), self).__init__(*args, **kwargs)
    return wrapper

class base():
    def __init__(self, n: int) -> None:
        print(f'Base: {n}')

class child(base):
    @default_init
    def __init__(self, n: int) -> None:
        pass
        
child(42)

输出:

Base: 42

第二部分/三部分:警告

警告:如果base本身调用了super(type(self), self),则此方法无效。

def default_init(func):
    def wrapper(self, *args, **kwargs) -> None:
        '''Warning: recursive calls.'''
        super(type(self), self).__init__(*args, **kwargs)
    return wrapper

class base():
    def __init__(self, n: int) -> None:
        print(f'Base: {n}')

class child(base):
    @default_init
    def __init__(self, n: int) -> None:
        pass
        
class child2(child):
    @default_init
    def __init__(self, n: int) -> None:
        pass
        
child2(42)

RecursionError: 调用Python对象时超出了最大递归深度。

第三部分:为什么不只使用普通的super()

但是为什么不只使用安全的普通super()呢?因为它不起作用,因为新的重新绑定的__init__来自类外部,需要使用super(type(self), self)

def default_init(func):
    def wrapper(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
    return wrapper

class base():
    def __init__(self, n: int) -> None:
        print(f'Base: {n}')

class child(base):
    @default_init
    def __init__(self, n: int) -> None:
        pass
        
child(42)

错误:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-9-6f580b3839cd> in <module>
     13         pass
     14 
---> 15 child(42)

<ipython-input-9-6f580b3839cd> in wrapper(self, *args, **kwargs)
      1 def default_init(func):
      2     def wrapper(self, *args, **kwargs) -> None:
----> 3         super().__init__(*args, **kwargs)
      4     return wrapper
      5 

RuntimeError: super(): __class__ cell not found

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接