在构造函数中运行可能会失败的代码是不良实践吗?

18 投票
8 回答
13217 浏览
提问于 2025-04-15 11:59

我的问题其实是关于设计的。

在Python中,如果你的“构造函数”里的代码出错了,那么这个对象就不会被定义。所以:

someInstance = MyClass("test123") #lets say that constructor throws an exception
someInstance.doSomething() # will fail, name someInstance not defined.

不过我遇到了一种情况,如果我把容易出错的代码从构造函数中移除,就会导致很多代码重复。简单来说,我的构造函数会填充几个属性(通过输入输出操作,这里很容易出问题),这些属性可以通过不同的获取方法来访问。如果我把代码移除,可能就会有10个获取方法,里面的代码大概都是这样的:

  1. 这个属性真的被设置了吗?
  2. 执行一些输入输出操作来填充这个属性
  3. 返回相关变量的内容

我不喜欢这样,因为所有的获取方法里都会有很多代码。与其这样,我选择在一个中心位置,也就是构造函数里执行我的输入输出操作,并填充所有的属性。

那么,有什么好的方法来做到这一点呢?

8 个回答

20

在C++中,把可能出错的代码放在构造函数里其实没什么问题。如果出现错误,你可以直接抛出一个异常来处理。如果这些代码是构造对象时必须要用到的,那就没有其他选择了(不过你可以把这些代码放到子函数里,或者更好的是放到子对象的构造函数里)。最糟糕的做法就是把对象构造一半,然后指望用户去调用其他函数来完成构造。

38

C++中的构造函数和Python中的__init__方法是有区别的。在C++中,构造函数的任务是创建一个对象。如果创建失败,就不会调用析构函数。因此,如果在抛出异常之前获取了任何资源,清理工作应该在构造函数退出之前完成。所以,有些人更喜欢两阶段构造法,大部分构造工作在构造函数外完成(这听起来有点麻烦)。

而Python的两阶段构造法要干净得多(先构造,再初始化)。不过,很多人把__init__方法(初始化器)和构造函数搞混了。实际上,Python中的构造函数叫做__new__。和C++不同的是,它不接收一个实例,而是返回一个实例。__init__的任务是初始化已经创建的实例。如果在__init__中抛出异常,析构函数__del__(如果有的话)会按预期被调用,因为在调用__init__时,对象已经被创建了(尽管它没有被正确初始化)。

回答你的问题:

在Python中,如果你的“构造函数”中的代码失败,最终对象不会被定义。

这并不完全正确。如果__init__抛出异常,对象是被创建了,但没有被正确初始化(例如,有些属性没有被赋值)。但是在异常抛出时,你可能没有任何对这个对象的引用,所以属性没有被赋值也没关系。只有析构函数(如果有的话)需要检查这些属性是否真的存在。

那么,正确的做法是什么呢?

在Python中,在__init__中初始化对象,不用担心异常。在C++中,使用RAII


更新 [关于资源管理]:

在垃圾回收的语言中,如果你在处理资源,尤其是有限的资源,比如数据库连接,最好不要在析构函数中释放它们。这是因为对象的销毁是非确定性的,如果你碰巧有一个引用循环(这并不总是容易判断),而且循环中的至少一个对象定义了析构函数,它们将永远不会被销毁。垃圾回收语言有其他处理资源的方法。在Python中,就是使用with语句

3

我不是Python开发者,但一般来说,在构造函数里最好避免复杂或容易出错的操作。解决这个问题的一种方法是,在你的类里添加一个“从文件加载”或“初始化”的方法,用来从外部来源填充对象。这个加载或初始化的方法需要在构造对象后单独调用。

撰写回答