Catch exceptions inside a class
Asked Answered
B

3

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...

Benzoic answered 10/7, 2012 at 19:26 Comment(7)
You could write a decorator for this.Elfrieda
You could also write a metaclass which would apply @phg's decorator to each method when it is defined.Clockwise
@Clockwise Submit that as an answer.Gwalior
possible duplicate of "outsourcing" exception-handling to a decoratorGwalior
@Marcin- on the other hand, the fact that the answer is a duplicate (using decorators) does not necessarily mean the question is a duplicate.Mojica
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 blog.ianbicking.org/2007/09/12/re-raising-exceptions for detail.Rialto
@Rialto 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"Aho
S
27

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...)

Sori answered 10/7, 2012 at 19:40 Comment(6)
+1 for "It's likely that you [really] don't want something like this".Rialto
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).Elfrieda
@phg I see where you're going with that -- hope you don't mind but I'll leave that for the OP.Sori
+1 useful for demonstration alone, but I want to emphasize to the OP again that, this is really a bad bad idea...Heptagonal
Also, check the concepts of "aspect oriented" programing, and aspect oriented code using Python - hey might be usefull to your real problemComplacency
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 eachLoginov
C
20

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
Clockwise answered 10/7, 2012 at 19:55 Comment(2)
+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.... coughSori
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?Zipper
B
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.

Bullyrag answered 10/7, 2012 at 19:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.