23

Is it possible to write an exception handler to catch the run-time errors generated by ALL the methods in class? I can do it by surrounding each one with try/except:

class MyError(Exception):
    def __init__(self, obj, method):
        print 'Debug info:', repr(obj.data), method.__name__
        raise

class MyClass:
    def __init__(self, data):
        self.data = data

    def f1(self):
        try:
            all method code here, maybe failing at run time
        except:
            raise MyError(self, self.f1)

I wonder if is there a more general way to achieve the same - for any error raising anywhere in the class. I would like to be able to access the class data to print some debug info. Also, how do I get the failing method name (f1 in the example)?

Update: thanks to all for the insighful answers, the decorator idea looks like the way to go. About the risk of trapping ALL the exceptions: a raise statement in the except branch should re-raise the exception without losing any information, doesn't it? That's why I put it in MyError also...

codeforester
  • 39,467
  • 16
  • 112
  • 140
dave9000
  • 333
  • 1
  • 2
  • 6
  • 4
    You could write a decorator for this. – phipsgabler Jul 10 '12 at 19:29
  • 5
    You could also write a metaclass which would apply @phg's decorator to each method when it is defined. – chepner Jul 10 '12 at 19:31
  • 1
    @chepner Submit that as an answer. – Marcin Jul 10 '12 at 19:32
  • 1
    possible duplicate of ["outsourcing" exception-handling to a decorator](http://stackoverflow.com/questions/9647021/outsourcing-exception-handling-to-a-decorator) – Marcin Jul 10 '12 at 19:37
  • @Marcin- on the other hand, the fact that the answer is a duplicate (using decorators) does not necessarily mean the question is a duplicate. – David Robinson Jul 10 '12 at 19:38
  • 1
    Per class exceptions for *all* exceptions for *all* methods in a class seems like a design error. So if `f1()` gets an IOError, it should report as a MyError? That's bound to lose information and confuse. See http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/ for detail. – msw Jul 10 '12 at 19:39
  • @msw it actually gets rid of the whole concept of exception being able to walk upwards if not handled and therefore being able to atleast localize unknown or not handleable errors/"exceptions of the regular program flow" – Sebastian Hoffmann Jul 10 '12 at 19:47

3 Answers3

26

Warning: if you want something like this, it's likely you don't... but if you really want to...

Something like:

import functools

def catch_exception(f):
    @functools.wraps(f)
    def func(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except Exception as e:
            print 'Caught an exception in', f.__name__
    return func

class Test(object):
    def __init__(self, val):
        self.val = val

    @catch_exception
    def calc():
        return self.val / 0

t = Test(3)
t.calc()

shows how to decorate individual functions. You can then create a class decorator to apply this decorator to each method (be careful of classmethod's/staticmethod's/properties etc...)

Jon Clements
  • 138,671
  • 33
  • 247
  • 280
  • 7
    +1 for "It's likely that you [really] don't want something like this". – msw Jul 10 '12 at 19:42
  • To make it _somehow_ more elegant, I'd add a `handler` parameter which is used inside the `except`. That would (used with care) even be useful to reduce repetition, like `@catch_exception(logAndRethrow)`. – phipsgabler Jul 10 '12 at 19:45
  • @phg I see where you're going with that -- hope you don't mind but I'll leave that for the OP. – Jon Clements Jul 10 '12 at 19:48
  • 3
    +1 useful for demonstration *alone*, but I want to emphasize to the OP again that, this is really a bad bad idea... – nye17 Jul 10 '12 at 19:51
  • Also, check the concepts of "aspect oriented" programing, and aspect oriented code using Python - hey might be usefull to your real problem – jsbueno Jul 13 '12 at 12:02
  • 14
    Could you explain the "you really don't want something like this" please? I have many scripts containing many classes, each with many methods. I'd like to know which class/method failed, without writing a try/except for each – Stephen Lead Jul 18 '14 at 03:45
19

Assuming you've got a decorator catch_exception as in @Jon Clement's answer...

class ErrorCatcher(type):
    def __new__(cls, name, bases, dct):
        for m in dct:
            if hasattr(dct[m], '__call__'):
                dct[m] = catch_exception(dct[m])
        return type.__new__(cls, name, bases, dct)

class Test(object):
    __metaclass__ = ErrorCatcher

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

    def calc(self):
        return self.val / 0

The metaclass applies catch_exception to everything that appears to be a method while it is defining Test.


In response to a comment regarding custom messages for each method, one could attach such a message (or even a callback function to generate a message) as an attribute:

class Test(object):
    __metaclass__ = ErrorCatcher
    def __init__(self, val):
        self.val = val

    def calc(self):
        return self.val / 0

    calc.msg = "Dividing by 0 is ill-advised"

The catch_exception decorator would look for a msg attribute on its argument and use it, if found, in handling the exception.

This approach could be extended; instead of a string, msg could be a mapping of exception types to strings. In either case, the string could be replaced (with support from catch_exception, of course) with an arbitrary callback function that takes, say, the raised exception as an argument.

def calc_handler(exc):
    # ...

calc.callback = calc_handler
chepner
  • 497,756
  • 71
  • 530
  • 681
  • 5
    +1 - Good example of `__metaclass__` - If the OP was going to end up shooting theirselves in the foot, at least between us we've provided examples to make sure they blow it right off.... *cough* – Jon Clements Jul 10 '12 at 20:01
  • I've of the best answers I've come across here is SO. Is there a way to customize the exception message for each method? – Keerthana Prabhakaran Nov 29 '17 at 05:29
3

A decorator would be a good solution here.

Here's an example of how you could do it:

import inspect

def catch_exception_decorator(function):
   def decorated_function:
      try:
         function()
      except:
         raise MyError(self.__class__, inspect.stack()[1][3])
   return decorated_function

class MyClass(object):
    def __init__(self):
         ...

    @catch_exception_decorator
    def f1(self):
         ...

@catch_exception_decorator on top of the function is a shortcut for f1 = catch_exception_decorator(f1).

Instead of doing self.class, you could also access class data from the instance, as long as you're not shadowing variables. inspect.stack()[1][3] is the function name of the current function. You can use these to create the exception attributes.

David Wong
  • 164
  • 3