Bad Practice to run code in constructor thats like

2019-03-12 13:27发布

my question is rather a design question. In Python, if code in your "constructor" fails, the object ends up not being defined. Thus:

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

I do have a situation though, where a lot of code copying would occur if i remove the error-prone code from my constructor. Basically my constructor fills a few attributes (via IO, where a lot can go wrong) that can be accessed with various getters. If I remove the code from the contructor, i'd have 10 getters with copy paste code something like :

  1. is attribute really set?
  2. do some IO actions to fill the attribute
  3. return the contents of the variable in question

I dislike that, because all my getters would contain a lot of code. Instead of that I perform my IO operations in a central location, the constructor, and fill all my attributes.

Whats a proper way of doing this?

8条回答
趁早两清
2楼-- · 2019-03-12 13:46

There is a difference between a constructor in C++ and an __init__ method in Python. In C++, the task of a constructor is to construct an object. If it fails, no destructor is called. Therefore if any resources were acquired before an exception was thrown, the cleanup should be done before exiting the constructor. Thus, some prefer two-phase construction with most of the construction done outside the constructor (ugh).

Python has a much cleaner two-phase construction (construct, then initialize). However, many people confuse an __init__ method (initializer) with a constructor. The actual constructor in Python is called __new__. Unlike in C++, it does not take an instance, but returns one. The task of __init__ is to initialize the created instance. If an exception is raised in __init__, the destructor __del__ (if any) will be called as expected, because the object was already created (even though it was not properly initialized) by the time __init__ was called.

Answering your question:

In Python, if code in your "constructor" fails, the object ends up not being defined.

That's not precisely true. If __init__ raises an exception, the object is created but not initialized properly (e.g., some attributes are not assigned). But at the time that it's raised, you probably don't have any references to this object, so the fact that the attributes are not assigned doesn't matter. Only the destructor (if any) needs to check whether the attributes actually exist.

Whats a proper way of doing this?

In Python, initialize objects in __init__ and don't worry about exceptions. In C++, use RAII.


Update [about resource management]:

In garbage collected languages, if you are dealing with resources, especially limited ones such as database connections, it's better not to release them in the destructor. This is because objects are destroyed in a non-deterministic way, and if you happen to have a loop of references (which is not always easy to tell), and at least one of the objects in the loop has a destructor defined, they will never be destroyed. Garbage collected languages have other means of dealing with resources. In Python, it's a with statement.

查看更多
老娘就宠你
3楼-- · 2019-03-12 13:49

Again, I've got little experience with Python, however in C# its better to try and avoid having a constructor that throws an exception. An example of why that springs to mind is if you want to place your constructor at a point where its not possible to surround it with a try {} catch {} block, for example initialisation of a field in a class:

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

If the constructor of MySecondClass throws an exception that you wish to handle inside MyClass then you need to refactor the above - its certainly not the end of the world, but a nice-to-have.

In this case my approach would probably be to move the failure-prone initialisation logic into an initialisation method, and have the getters call that initialisation method before returning any values.

As an optimisation you should have the getter (or the initialisation method) set some sort of "IsInitialised" boolean to true, to indicate that the (potentially costly) initialisation does not need to be done again.

In pseudo-code (C# because I'll just mess up the syntax of 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;
        }
    }
}

This is of course not thread-safe, but I don't think multithreading is used that commonly in python so this is probably a non-issue for you.

查看更多
神经病院院长
4楼-- · 2019-03-12 13:51

It is not bad practice per se.

But I think you may be after a something different here. In your example the doSomething() method will not be called when the MyClass constructor fails. Try the following code:

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"

It should print:

test123
except

For your software design you could ask the following questions:

  • What should the scope of the someInstance variable be? Who are its users? What are their requirements?

  • Where and how should the error be handled for the case that one of your 10 values is not available?

  • Should all 10 values be cached at construction time or cached one-by-one when they are needed the first time?

  • Can the I/O code be refactored into a helper method, so that doing something similiar 10 times does not result in code repetition?

  • ...

查看更多
趁早两清
5楼-- · 2019-03-12 13:52

I'm not a Python developer, but in general, it's best to avoid complex/error-prone operations in your constructor. One way around this would be to put a "LoadFromFile" or "Init" method in your class to populate the object from an external source. This load/init method must then be called separately after constructing the object.

查看更多
可以哭但决不认输i
6楼-- · 2019-03-12 13:55

seems Neil had a good point: my friend just pointed me to this:

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

which is basically what Neil said...

查看更多
Bombasti
7楼-- · 2019-03-12 14:05

One common pattern is two-phase construction, also suggested by Andy White.

First phase: Regular constructor.

Second phase: Operations that can fail.

Integration of the two: Add a factory method to do both phases and make the constructor protected/private to prevent instantation outside the factory method.

Oh, and I'm neither a Python developer.

查看更多
登录 后发表回答