Python和C++构造函数的区别

14

最近我在学习Python,浏览了优秀的Dive into Python,作者在这里指出__init__方法虽然通常像构造函数一样工作,但在技术上不是构造函数。

我有两个问题:

  1. C++如何构造一个对象以及Python如何“构造”一个对象之间有哪些差异?

  2. 什么使得构造函数成为构造函数,__init__方法又是如何未达到此标准的?


我不确定我是否同意2...由于Python是一种垃圾收集语言,C++中构造函数的“第一”步(分配内存)不必执行,并且属性可以分配给实例。因此,在我看来,__init__是一个构造函数,因为它分配值给属性并填充/完成实例。 - Rafe Kettler
2
@Rafe Kettler:在C++中,构造函数并不分配内存,它们只是将原始内存(先前已分配)初始化并转换为对象。 - David Rodríguez - dribeas
@David:好的,谢谢你的澄清。我想他们是为那个对象分配内存,而不是一般地分配内存。 - Rafe Kettler
@Rafe Kettler:不,构造函数运行之前必须预先分配内存。当您调用new时,它会为您分配内存,然后在该内存上运行构造函数。重要的是,分配和构造是完全不同的操作。请注意,任何对象(一般而言)都可以在堆栈或堆中分配,并且构造函数是唯一的。 - David Rodríguez - dribeas
4个回答

15

作者区分的是,就Python语言而言,在进入__init__之前,您已经拥有了指定类型的有效对象。因此,它不是一个“构造函数”,因为在C++中和理论上,构造函数将无效的、预构造的对象转换为该类型的“适当”完成的对象。

基本上,Python中的__new__被定义为返回“新对象实例”,而C++ new操作符只返回一些内存,这还不是任何类的实例。

然而,在Python中,__init__可能是您首次创建一些重要类不变量(仅从起始点来看)。所以就用户类而言,它可能就是构造函数。只是Python运行时不关心这些不变式。如果您愿意,它对于构成构造好的对象具有非常低的标准。

我认为作者的观点很公正,并且它确实是关于Python创建对象方式的有趣评论。不过这是一个微妙而微小的区别,我怀疑将__init__称为构造函数会导致错误代码。

此外,我注意到Python文档将__init__称为构造函数(http://docs.python.org/release/2.5.2/ref/customization.html)

  

作为构造函数的一个特殊限制,不能返回任何值

.....所以如果将__init__视为构造函数会导致任何实际问题,那么Python就会遇到麻烦!

Python和C ++构建对象方式有一些相似之处。两者都调用具有相对简单职责的函数(对象实例的__new__ vs原始内存的某个版本的operator new),然后两者都调用可以更多地初始化对象进入可用状态的函数(__init__vs构造函数)。

实际差异包括:

  • 在C++中,如果需要,基类的无参构造函数会按照适当的顺序自动调用,而在Python的__init__中,您需要在自己的__init__中显式地初始化您的基类。即使在C++中,如果基类构造函数有参数,您也需要指定它。

  • 在C++中,当构造函数抛出异常时,您有整个机制来处理已经构造的子对象的析构函数调用。在Python中,我认为运行时(最多)只调用__del__

此外,还有一个区别是__new__不仅仅分配内存,它还必须返回一个实际的对象实例。然而,在Python代码中,原始内存并不是一个真正适用的概念。


运行时实际上并不调用 __del__,它只是向上传播异常,并在最后一个对正在初始化的对象的引用消失后(最终)销毁该对象。__del__ 可能 在此之前被调用,但实际上并未用于执行任何析构操作;__del____init__ 的对应项,而不是 __new__ :) - Thomas Wouters
@Thomas:如果__del__真的是__init__的对应物,那么我期望它被定义为只有在__init__完成时才会被调用。当然,它可能会释放在__init__或类的任何其他方法中分配的系统资源。我的有限理解是,在CPython中,无论__init__是否完成甚至是否被调用,都会在对象被销毁时调用__del__。而且,未能成功执行__init__的对象将立即被销毁,尽管这是CPython引用计数的实现细节。这样对吗? - Steve Jessop
所有这些都有一个警告,即如果对象在程序退出时仍然存在,则可能根本不会调用__del__。但是我并不想过多地关注__del__。我从未使用过它,并且就其保证的行为而言,它对我来说似乎与Java finalizer一样有用,即不是很有用。 - Steve Jessop
__del__不是__init__严格对应物。它不与__init__成功执行绑定。它是__init__的对应物,因为它是一个可选的钩子,在对象销毁之前被调用,就像__init__是一个可选的钩子,在对象构造之后右侧被调用。 __del__并不会被调用以执行对象销毁,而且__del__方法实际上可以防止对象销毁(通过在某处存储对self的引用)。 是的,这真的没有用,并且它可能会创建无法清理的引用循环,这非常糟糕。依靠Python来进行清理更好。 - Thomas Wouters

3
在Python中,通过__new__创建对象,并通过__init__修改该通用默认对象。而__init__是一个普通方法,特别的是它可以被虚拟地调用,从__init__中调用的方法也会被虚拟调用。
在C++中,为某个对象分配了原始内存(raw memory),可以是静态分配、函数调用栈上分配、使用operator new动态分配,或者作为另一个对象的一部分分配。然后实例化类型的构造函数将原始内存初始化为适当的值。给定类的构造函数自动调用基类和成员的构造函数,因此构造保证是“自下而上”的构造方式,优先创建部分。
C++添加了语言支持来特别重要地体现构建部件的思想:
  • 如果构造函数失败(通过抛出异常),则已成功构造的部分将被自动销毁,并且该对象的内存将被自动释放。
  • 在类型T的构造函数体执行期间,该对象的类型为T,因此对于虚拟方法的调用将解析为T类型的对象(此时确实如此),其中T可以是您实例化的类的基类。
第一个要点意味着,对于正确设计的C ++类,当您手头有一个对象时,它保证是可用的。如果构造失败,则您不会得到手头的对象。
此外,C++的规则旨在确保在大多数导出类T的每个对象中,只有一个T构造函数调用。我曾将其称之为单构造函数调用保证(single constructor call guarantee)。它没有在标准的任何地方指定,您可以通过使用语言的非常低级的功能来挫败它,但是它存在,并且标准的详细规则旨在实现它(这与您不会在任何地方找到有关语句分号结束的单个规则非常相似,然而各种语句的无数语法规则共同产生了一个简单的高级规则)。
单构造函数调用保证、自动清理保证和基类构造函数执行时对象类型的更改,可能是与Python对象构造最重要的三个区别。
还有很多需要说的,但我认为这些是最重要的内容。
祝好!

2
在许多其他语言中,构造函数会为正在构建的对象分配空间;在Python中,这是分配器方法__new__()的工作。__init__()只是一个初始化方法。

1
C++中的构造函数不会为对象分配空间,这是operator new的工作。或者在放置new的退化情况下,实际上是调用者的工作告诉new版本内存所在的位置,以便它可以将该值回显给您。 - Steve Jessop

-1

这种差异是由于Python的动态类型。与C++不同,C++中变量是带有类型声明并在内存中分配的,而Python的变量是在运行时赋值时创建的。C++的无参构造函数会自动调用,以便初始化数据成员。在Python中,它是按需完成的,并且__init__是通过继承树查找的,只有最低的一个被调用一次。如果需要超类数据属性,则像C++ initialization list那样显式调用super().__init__()。下面是一个示例,其中初始化了Base.s:

class   Base:
    def __init__(self, base):
        print("Base  class init called")
        self.base = base
class   Super(Base):
    def __init__(self, base):
        print("Super class init called")
        #super(Super, self).__init__(base)
        super().__init__(base)
class   Sub(Super):
    def __init__(self, base):
        print("Default __init__ is called")
        super().__init__(base)

sub = Sub({"base3": 3, "base4": 4})
print(sub.__dict__, sub.base)

输出:

Default __init__ is called
Super class init called
Base  class init called
{'base': {'base3': 3, 'base4': 4}} {'base3': 3, 'base4': 4}

此外,init 就像普通函数一样,可以随后作为函数调用:
Sub.__init__(sub, 'abc')
print(sub.__dict__, sub.base)

输出:

Default __init__ is called
Super class init called
Base  class init called
{'base': 'abc'} abc

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