简体   繁体   中英

Add extra information to all python exceptions

After another terrible bug hunt, I am wondering the following: Is it possible to add some extra information to all exceptions, for example the name of an object. This would increase the readability of the errors a lot and make looking for bugs (or input errors) a lot quicker. This is especially the case if one has many objects which are from the same class and therefore share much code, but have different attributes. In this case it can be very useful if the error message also states the name of the object in the error.

A simplfied example: I am trying to simulate different types of facilities, a pig farm and a cow farm. These are the same class, but do have different attributes. In the simulation many facilities are made and if an exception is raised, it would be very helpful if the name of the object is added to the exception.

class facility():
    def __init__(self, name):
        self.name = name
        self.animals = []

farms = []
farms.append(facility('cow_farm'))
farms.append(facility('pig_farm'))
print farms[0].stock

This would yield

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: facility instance has no attribute 'stock'

But I would like to add the name of the facility:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: facility instance has no attribute 'stock'
  Name of object: cow_farm

I tried something like

def print_name(exception):
    try:
        print self.name
    except AttributeError:
        pass
    raise exception

@print_name
Exception

But that doesn't work. Is it possible to do such a thing, or are there good reasons not to do this?

If you want to handle errors and add information, you could do it as follows:

farm = farms[0]
try:
    print farm.stock
except AttributeError:
    raise AttributeError("{} has no attribute 'stock'".format(farm.name))

However, it might be wiser to add the empty stock in __init__ to avoid this error.

You should never use a bare except , as it hides useful information from you ( particularly when developing and debugging!) Generally, each try block should be as short as possible, preferably doing only one thing. If multiple errors could stem from a single try block, you can add multiple handlers:

try:
    print farm.stock["hay"]
except AttributeError:
    raise AttributeError("{} has no attribute 'stock'".format(farm.name))
except KeyError:
    raise KeyError("{} has no 'hay' in 'stock'".format(farm.name))

(Although note that adding self.stock in __init__ and checking if "hay" in farm.stock: would save you from this error handling.)

If an error happens that you weren't expecting, it is generally best for that error to propagate up the call stack until it is explicitly handled or you get to see it. Otherwise, you are heading for this foolish anti-pattern:

def some_func(*args, **kwargs):
    try:
        # all of some_func's content goes here
    except:
        raise Exception("Something went wrong in some_func().")

Which is no use to you and extremely frustrating for anyone trying to use your code.

If you want to handle AttributeError s like this at the class level, you could do:

class Facility(object):

    def __init__(self, ...):
        ...

    def __getattr__(self, key):
        """Called on attempt to access attribute instance.key."""
        if key not in self.__dict__:
            message = "{} instance '{}' has no attribute '{}'."
            message = message.format(type(self).__name__,
                                     self.name, key)
            raise AttributeError(message)
        else:
            return self.__dict__[key]

Then you will get

>>> farm = Facility("pig farm")
>>> print farm.stock
...
"AttributeError: Facility instance 'pig farm' has no attribute 'stock'."

If you want to use this pattern with multiple classes, you can make a superclass:

class ProtectedAttrs(object):

    def __init__(self, name):
        self.name = name

    def __getattr__(self, key):
        ...

class Facility(ProtectedAttrs):

    def __init__(self, name):
        super(Facility, self).__init__(name)
        self.animals = []

This sort of thing will work for some types of error. However, I am not aware of any general way to handle all errors with references to the instance involved.

Most of exceptions contain message attribute, that give you additional information about error

In [163]: try:
   .....:     farm = []
   .....:     farm.stock
   .....: except AttributeError as err:
   .....:     print err.message
   .....:     
'list' object has no attribute 'stock'

Exception traceback points you to a code line, so usually it is not hard to figure out the problem

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM