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


155

为什么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__方法,甚至可以自动搜索子类并装饰它们。
osa 2015年

2
@osa,听起来真的是个好主意。您想在装饰器部分描述更多吗?
滇盛

1
@osa是的描述更多!
查理·帕克

Answers:


163

Python __init__与其他语言的构造函数之间的关键区别在于,__init__不是构造函数:它是一个初始化程序(实际的构造函数(如果有,但是请参阅下文;-)是__new__并且再次完全不同。虽然构建所有超(,毫无疑问,这样做,你继续向下构建“之前”)显然是说你的一部分构建一个子类的实例,这显然是不适合的情况下初始化,因为在许多用例中,超类的初始化需要被跳过,更改和控制-发生在子类初始化的“中间”,如果发生的话,等等。

基本上,出于完全相同的原因,初始化程序的超类委派在Python中不是自动的,此类委派对于任何其他方法也不是自动的-请注意,那些“其他语言” 对任何其他方法都不会自动进行超类委派其他方法... 只是针对构造函数(如果适用,也应包含析构函数),正如我提到的,这不是 Python的__init__方法。(的行为__new__也很特殊,尽管实际上与您的问题没有直接关系,因为它__new__是一个奇特的构造函数,实际上并不一定要构造任何东西-可以很好地返回一个现有实例,甚至一个非实例...显然Python为您提供了很多比您要记住的“其他语言”,对机械的控制更多,它本身没有自动委派__new__!-)。


7
受到鼓舞,因为对我来说,真正的好处是您提到的在子类的初始化的任何时候(或根本没有调用超类的_ init()_的能力。
kindall 2010年

53
-1代表“ __init__不是构造器...实际构造器是... __new__”。如您自己所述,__new__其行为与其他语言的构造函数不同。__init__实际上非常相似(在创建新对象之后,在分配该对象之后,会在新对象上设置成员变量,它被称为),并且几乎始终是实现您要放入其他语言的功能的地方构造函数。因此,只需将其称为构造函数即可!

8
我也认为这是一个相当荒谬的说法:“ 构造所有超类……显然是在说您要构造子类的实例,而初始化显然不是这种情况”。在“构造/初始化”一词中,没有任何内容可以使任何人“明白”这一点。并且__new__也不会自动调用超类__new__。因此,您声称关键区别在于构造必然涉及超类的构造,而初始化并不与__new__构造函数的主张不一致。

36
实际上,从docs.python.org/reference/datamodel.html#basic-customization的python文档__init__获取:“作为对构造函数的特殊约束,可能不会返回任何值;这样做将导致在运行时引发TypeError ”(强调我的意思)。因此,这__init__是一个正式的构造函数。

3
在Python / Java术语中,__init__称为构造函数。此构造函数是在对象完全构造并初始化为默认状态(包括其最终运行时类型)之后调用的初始化函数。它不等同于C ++构造函数,该构造函数是在具有未定义状态的静态类型的已分配对象上调用的。这些也与有所不同__new__,因此我们实际上至少有四种不同的分配/构造/初始化函数。语言使用混合的术语,而重要的部分是行为而不是术语。
Elazar

36

当人们模仿“ Python禅”时,我有些尴尬,好像这是任何事情的正当理由。这是一种设计理念;特定的设计决策总是可以用更具体的术语来解释-必须如此,否则“ Zen of 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__


6
这个例子有什么特别之处吗?静态语言也可以做到这一点
吉恩

18

Java和C ++ 要求由于内存布局而调用基类构造函数。

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

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

因此,对于Python子类而言,不调用其基类构造函数是很有意义的。如果需要,它可以只添加属性本身。对于层次结构中的每个类,没有为给定数目的字段保留空间,并且通过BaseClass方法中的代码添加的属性与通过方法中的代码添加的属性之间没有区别SubClass

如果像通常一样,SubClass实际上确实希望BaseClass在继续进行自己的自定义之前设置所有的不变式,那么可以调用BaseClass.__init__()(或使用)super,但这是很复杂的,并且有时会出现自己的问题。但是您不必。您可以在之前,之后或使用其他参数来执行此操作。地狱,如果你想的话,可以BaseClass.__init__完全从另一个方法而不是__init__; 调用。也许您正在进行一些奇怪的懒惰初始化操作。

Python通过保持简单而实现了这种灵活性。通过编写在__init__上设置属性的方法来初始化对象self。而已。它的行为与方法完全一样,因为它正是方法。对于必须首先完成的事情,或者如果您不执行其他操作会自动发生的事情,没有其他奇怪而又不直观的规则。它唯一需要服务的目的是成为一个在对象初始化期间执行的钩子,以设置初始属性值,而它正是这样做的。如果您希望它做其他事情,则可以在代码中显式地编写。


2
对于C ++,它与“内存布局”无关。可以使用C ++实现与Python提供的初始化模型相同的初始化模型。使用C ++进行构造/销毁的唯一原因是因为设计决策提供了可靠且行为良好的资源管理工具(RAII),并且可以自动生成(意味着更少的代码和人为错误)由编译器提供,因为它们的规则(调用顺序)是严格定义的。不确定,但是Java很可能只是遵循这种方法而成为另一种类似于POLA C的语言。
亚历山大·舒卡耶夫

10

“显式比隐式好。” 同样的道理表明我们应该明确地写出“自我”。

最后,我认为这是有好处的-您能列举一下Java关于调用超类的构造函数的所有规则吗?


8
我在很大程度上同意您的意见,但是Java的规则实际上非常简单:除非您特别要求使用其他参数,否则将调用no-arg构造函数。
劳伦斯·贡萨尔维斯

1
@Laurence-父类未定义无参数构造函数时会发生什么?当无参数构造函数受保护或私有时会发生什么?
Mike Axiak 2010年

8
如果您尝试显式调用它,将会发生完全相同的事情。
劳伦斯·贡萨尔维斯


7

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

如果自动调用了构造函数,则需要另一页至少具有相同长度的页面,以解释其发生的顺序。那将是地狱...


这是正确的答案。Python将需要定义在子类和超类之间传递的参数的语义。太糟糕了,这个答案不能被接受。也许它通过一些例子表明了问题?
加里·魏斯

5

为了避免混淆,知道__init__()child_class没有__init__()类时可以调用base_class 方法是很有用的。

例:

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

实际上,__init__()当在子类中找不到它时,python中的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.

3

也许__init__是子类需要重写的方法。有时,子类在添加特定于类的代码之前需要运行父级函数,而有时,它们需要在调用父级函数之前设置实例变量。由于Python不可能知道何时最适合调用这些函数,因此不应该猜测。

如果这些都不影响您,请考虑这__init__只是另一个功能。如果有问题的函数dostuff代替了,您是否仍然希望Python在父类中自动调用相应的函数?


2

我相信这里一个非常重要的考虑因素是,通过自动调用super.__init__(),您可以按设计禁止在何时调用该初始化方法以及使用哪些参数。避免自动调用它,并要求程序员明确地执行该调用,需要很大的灵活性。

毕竟,仅因为类B派生自类A并不意味着A.__init__()可以或应该使用与相同的参数进行调用B.__init__()。将调用明确化意味着程序员可以例如B.__init__()使用完全不同的参数进行定义,使用该数据进行一些计算,A.__init__()使用适合该方法的参数进行调用,然后进行一些后处理。如果A.__init__()B.__init__()B.__init__()执行之前或执行之后隐式调用,则很难获得这种灵活性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.