Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use an object even though `__init__()` raises an exception?

I'm in a situation where some meager important parts of a classes __init__ method could raise an exception. In that case I want to display an error message but carry on using the instance.

A very basic example:

class something(object):
    def __init__(self):
        do_something_important()
        raise IrrelevantException()

    def do_something_useful(self):
        pass

try:
    that_thing = something()
except IrrelevantException:
    print("Something less important failed.")

that_thing.do_something_useful()

However, the last line does not work, because that_thing is not defined. Strange thing is, I could swear I've done things like this before and it worked fine. I even thougt about ways to keep people from using such an unfinished instance, because I found out it gets created even in case of errors. Now I wanted to use that and it does not work. Hmmm...?!?

PS: something was written by myself, so I'm in control of everything.

like image 256
Bachsau Avatar asked Sep 01 '25 22:09

Bachsau


2 Answers

You can accomplish this by calling object.__new__() to create the object. Then after that call __init__() to create the object.

This will execute all of the code possible.

class IrrelevantException(Exception):
    """This is not important, keep executing."""
    pass

class something(object):
    def __init__(self):
        print("Doing important stuff.")
        raise IrrelevantException()

    def do_something_useful(self):
       print("Now this is useful.")

that_thing = object.__new__(something) # Create the object, does not call __init__
try:
    that_thing.__init__() # Now run __init__
except IrrelevantException:
    print("Something less important failed.")

that_thing.do_something_useful() # And everything that __init__ could do is done.

EDIT, as @abarnert pointed out. This code does presume that __init__() is defined, but __new__() is not.

Now if it can be assumed that __new__() will not error, it can replace object.__new__() in the code.

However, if there is an error in object.__new__(), there is no way to both create the instance, and have the actions in __new__() applied to it.

This is because __new__() returns the instance, versus __init__() which manipulates the instance. (When you call something(), the default __new__() function actually calls __init__() and then quietly returns the instance.)

So the most robust version of this code would be:

class IrrelevantException(Exception):
    """This is not important, keep executing."""
    pass

class something(object):
    def __init__(self):
        print("Doing important stuff.")
        raise IrrelevantException()

    def do_something_useful(self):
       print("Now this is useful.")

try:
    that_thing = something.__new__(something) # Create the object, does not call __init__
except IrrelevantException:
    # Well, just create the object without calling cls.__new__()
    that_thing = object.__new__(something)
try:
    that_thing.__init__() # Now run __init__
except IrrelevantException:
    print("Something less important failed.")

that_thing.do_something_useful()

So, meanwhile both of these answer the question, this latter one should also help in the (admittedly rare) case where __new__() has an error, but this does not stop do_something_useful() from working.

like image 79
The Matt Avatar answered Sep 03 '25 16:09

The Matt


From a comment:

PS: something was written by myself, so I'm in control of everything.

Well, then the answer is obvious: just remove that raise IrrelevantException()

Of course your real code probably doesn't have raise IrrelevantException, but instead a call to some dangerous_function() that might raise. But that's fine; you can handle the exception the same way you do anywhere else; the fact that you're inside an __init__ method makes no difference:

class something(object):
    def __init__(self):
        do_something_important()
        try:
            do_something_dangerous()
        except IrrelevantException as e:
            print(f'do_something_dangerous raised {e!r}')
        do_other_stuff_if_you_have_any()

That's all there is to it. There's no reason your __init__ should be raising an exception, and therefore the question of how to handle that exception never arises in the first place.


If you can't modify something, but can subclass it, then you don't need anything fancy:

class IrrelevantException(Exception):
    pass

def do_something_important():
    pass

class something(object):
    def __init__(self):
        do_something_important()
        raise IrrelevantException()

    def do_something_useful(self):
        pass

class betterthing(something):
    def __init__(self):
        try:
            super().__init__() # use 2.x style if you're on 2.x of course
        except IrrelevantException:
            pass # or log it, or whatever
        # You can even do extra stuff after the exception

that_thing = betterthing()
that_thing.do_something_useful()

Now do_something_important got called, and a something instance got returns that I was able to save and call do_something_useful on, and so on. Exactly what you were looking for.


You could of course hide something behind betterthing with some clever renaming tricks:

_something = something
class something(_something):
    # same code as above

… or just monkeypatch something.__init__ with a wrapper function instead of wrapping the class:

_init = something.__init__
def __init__(self):
    try:
        _init(self)
    except IrrelevantException:
        pass
something.__init__ = __init__

But, unless there's a good reason that you can't be explicit about the fact that you're adding a wrapper it, it's probably better to be explicit.

like image 45
abarnert Avatar answered Sep 03 '25 16:09

abarnert