在构造函数中运行可能失败的代码的不良做法?

时间:2009-06-02 08:05:55

标签: python oop exception-handling constructor

我的问题是设计问题。 在Python中,如果“构造函数”中的代码失败,则对象最终不会被定义。因此:

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

我确实有这样的情况,如果我从构造函数中删除容易出错的代码,会发生很多代码复制。基本上我的构造函数填充了一些属性(通过IO,很多可能出错),可以使用各种getter访问。如果我从构造函数中删除代码,我将有10个带有复制粘贴代码的getter,如:

  1. 是属性真的设置了吗?
  2. 执行一些IO操作以填充属性
  3. 返回有问题的变量的内容
  4. 我不喜欢这样,因为我的所有getter都会包含很多代码。而不是我在中心位置,构造函数中执行我的IO操作,并填充我的所有属性。

    这是一种正确的方法吗?

8 个答案:

答案 0 :(得分:34)

C ++中的构造函数与__init__方法之间存在差异 在Python中。在C ++中,构造函数的任务是构造一个对象。如果失败了, 没有析构函数被调用。因此,如果在之前获得任何资源 抛出异常,应该在退出构造函数之前完成清理。 因此,有些人更喜欢两相结构,大部分结构都已完成 在构造函数之外(嗯)。

Python有一个更清洁的两阶段构造(构造,然后 初始化)。但是,很多人混淆了__init__方法(初始化程序) 用构造函数。 Python中的实际构造函数称为__new__。 与C ++不同,它不需要实例,但是 返回一个。 __init__的任务是初始化创建的实例。 如果在__init__中引发异常,则析构函数__del__(如果有) 将按预期调用,因为在调用__init__时已经创建了对象(即使它没有正确初始化)。

回答你的问题:

  

在Python中,如果代码在你的   “构造函数”失败,对象结束   没有定义。

这不完全正确。如果__init__引发异常,则对象为 已创建但未正确初始化(例如,某些属性不是 分配)。但在它被提出的时候,你可能没有任何参考 这个对象,所以未分配属性的事实并不重要。只有析构函数(如果有的话)需要检查属性是否确实存在。

  

这是一种正确的方法吗?

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


更新 [关于资源管理]:

在垃圾收集语言中,如果您正在处理资源,尤其是数据库连接等有限资源,最好不要在析构函数中释放它们。 这是因为对象以非确定性方式被破坏,如果你发生了 有一个引用循环(这并不总是很容易辨别),并且循环中至少有一个对象定义了析构函数,它们永远不会被销毁。 垃圾收集语言还有其他处理资源的方法。在Python中,它是with statement

答案 1 :(得分:20)

至少在C ++中,在构造函数中放置容易出错的代码没有任何问题 - 如果发生错误,您只需抛出异常。如果需要代码来正确构造对象,那么实际上没有其他选择(尽管您可以将代码抽象为子函数,或者更好地将其抽象为子对象的构造函数)。最糟糕的做法是半构造对象,然后期望用户调用其他函数以某种方式完成构造。

答案 2 :(得分:4)

我不是Python开发人员,但总的来说,最好避免构造函数中出现复杂/容易出错的操作。解决此问题的一种方法是在类中放置“LoadFromFile”或“Init”方法,以从外部源填充对象。然后必须在构造对象后单独调用此load / init方法。

答案 3 :(得分:4)

这本身并不差。

但我认为你可能会追求一些不同的东西。在您的示例中,当MyClass构造函数失败时,不会调用doSomething()方法。请尝试以下代码:

class MyClass:
def __init__(self, s):
    print s
    raise Exception("Exception")

def doSomething(self):
    print "doSomething"

try:
    someInstance = MyClass("test123")
    someInstance.doSomething()
except:
    print "except"

应该打印:

test123
except

对于您的软件设计,您可以提出以下问题:

  • someInstance变量的范围应该是多少?谁是它的用户?他们的要求是什么?

  • 如果您的10个值之一不可用,应该在何处以及如何处理错误?

  • 是否应该在施工时缓存所有10个值,还是在第一次需要时逐个缓存?

  • 可以将I / O代码重构为辅助方法,这样做10次类似的操作就不会导致代码重复吗?

  • ...

答案 4 :(得分:3)

一种常见的模式是两阶段结构,安迪怀特也建议。

第一阶段:常规构造函数。

第二阶段:可能失败的操作。

两者的集成:添加工厂方法来执行两个阶段并使构造函数受保护/私有以防止在工厂方法之外进行即时化。

哦,我不是Python开发人员。

答案 5 :(得分:0)

如果用于初始化各种值的代码非常广泛,那么复制它是不可取的(听起来就像你的情况)我个人会选择将所需的初始化放入私有方法中,添加一个标志来指示初始化是否已经发生,如果尚未初始化,则使所有访问者调用初始化方法。

在线程场景中,您可能必须添加额外的保护,以防初始化仅允许有效语义发生一次(由于您正在处理文件,可能会或可能不是这种情况)。

答案 6 :(得分:0)

同样,我对Python没什么经验,但是在C#中,尝试避免使用抛出异常的构造函数会更好。想到这一点的一个例子是,如果你想把你的构造函数放在一个无法用try {} catch {}块包围它的位置,例如在类中初始化一个字段:

class MyClass
{
    MySecondClass = new MySecondClass();
    // Rest of class
}

如果MySecondClass的构造函数抛出了一个你希望在MyClass中处理的异常,那么你需要重构上面的内容 - 它肯定不是世界末日,而是一个很好的东西。

在这种情况下,我的方法可能是将容易出错的初始化逻辑转换为初始化方法,并让getters在返回任何值之前调用该初始化方法。

作为优化,你应该让getter(或初始化方法)将某种“IsInitialised”布尔值设置为true,以表示不需要再次进行(可能代价高昂的)初始化。

在伪代码中(C#,因为我只是弄乱了Python的语法):

class MyClass
{
    private bool IsInitialised = false;

    private string myString;

    public void Init()
    {
        // Put initialisation code here
        this.IsInitialised = true;
    }

    public string MyString
    {
        get
        {
            if (!this.IsInitialised)
            {
                this.Init();
            }

            return myString;
        }
    }
}

这当然不是线程安全的,但我认为通常在python中使用多线程,所以这对你来说可能不是问题。

答案 7 :(得分:0)

似乎尼尔有一个好点:我的朋友刚刚指出我:

http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

这基本上就是尼尔说的......