Welcome, guest | Sign In | My Account | Store | Cart

Checked exceptions are one of the most debated features of the Java language. A checked exception is a contract between a function that declares and throws an exception and another function that calls those function and has to handle the exception in its body. This recipe presents a checked exception implementation for Python using a pair of decorators @throws(Exc) and @catches(Exc,...). Whenever a @throws decorated function is called it has to be inside of a function that is decorated by @catches. Otherwise an UncheckedExceptionError will be raised - unless the declared exception Exc is raised.

Python, 142 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import sys

__all__ = ["__CHECKING__","throws", "catches"]

__CHECKING__ = True  # (de)activate checking. 

def re_raise(exc, msg, traceback):
    raise exc, msg, traceback

class UncheckedExceptionError(Exception):pass

class ExceptionChecker(object):
    def __init__(self):
        self._id = 0
        self._exceptions = {}
        
    def set_attention(self, exc):
        self._id +=1
        try:                
            self._exceptions[exc].append(self._id)
        except KeyError:
            self._exceptions[exc] = [self._id]
        return self._id            
            
    def remove_attention(self, exc, id):
        try:
            self._exceptions[exc].remove(id)
        except (KeyError, AttributeError):
            pass
        
    def throwing(self, exc):        
        if not self._exceptions.get(exc):            
            raise UncheckedExceptionError(exc)
        
exc_checker = ExceptionChecker()

def catches(exc, handler = re_raise):
    '''
    Function decorator. Used to decorate function that handles exception class exc.
    
    An optional exception handler can be passed as a second argument. This exception
    handler shall have the signature
            handler(exc, message, traceback).
    '''
    if not __CHECKING__:
        return lambda f:f    
    def wrap(f):
        def call(*args, **kwd):            
            try:
                ID = exc_checker.set_attention(exc)
                res = f(*args,**kwd)
                exc_checker.remove_attention(exc, ID)
                return res
            # handle checked exception
            except exc, e:
                exc_checker.remove_attention(exc, ID)
                traceback = sys.exc_info()[2]                                                    
                return handler(exc, str(e), traceback.tb_next.tb_next)
            # re-raise unchecked exception but remove checked exeption info first
            except Exception, e:   
                exc_checker.remove_attention(exc, ID)
                traceback = sys.exc_info()[2]                                                                    
                raise e.__class__, e.args, traceback.tb_next.tb_next                
        call.__name__ = f.__name__
        return call
    return wrap

def throws(exc):
    '''
    throws(exc)(func) -> func'
    
    Function decorator. Used to decorate a function that raises exc.
    '''
    if not __CHECKING__:
        return lambda f:f
    def wrap(f):        
        def call(*args, **kwd):
            res = f(*args,**kwd)
            # raise UncheckedExceptionError if exc is not automatically 
            # registered by a function decorated with @catches(exc);
            # otherwise do nothing
            exc_checker.throwing(exc)  
            return res
        call.__name__ = f.__name__
        return call    
    return wrap

#
#
#  Test
#
# 

def test():
    @throws(ZeroDivisionError)
    def divide(x,y):
       return x/y

    def test1():     # uses divide() but does not implement exception handling
        return divide(2,7)

    try:
       test1()
    except UncheckedExceptionError, e:
       print "Raises UncheckedExceptionError(%s) -> OK"%str(e)

    @catches(ZeroDivisionError)   
    def test2(x,y):     
        return divide(x,y)  # uses divide() correctly. Uses default exception
                            # handler that re-raises the same exception
    assert test2(4,2) == 2

    try:
       test2(1,0)
    except ZeroDivisionError:
       print "Raises ZeroDivisionError -> OK"

    @catches(ZeroDivisionError, handler = lambda exc, msg, traceback: "zero-div")   
    def test3(x,y):     
        return divide(x,y)  # defining handler that returns string "zero-div"
                            # when division by zero 
    assert test3(1,0) == "zero-div"
    
    # declaring two exceptions
    @throws(TypeError)
    @throws(ZeroDivisionError)
    def divide(x,y):
       return x/y 

    try:
        test2(3,2)
    except UncheckedExceptionError, e:
        print "Raises UncheckedExceptionError(%s) -> OK"%str(e)

    @catches(TypeError)   
    @catches(ZeroDivisionError)   
    def test4(x,y):     
        def indirection(x,y):      # indirect call permitted  
            return divide(x,y)     
        return indirection(x,y)
                            
    assert test4(4,2) == 2

Using the default implementation of @catches by means of the re_raise handler bypasses the raised exception. The @catches decorator becomes not much more than a reminder. Moreover it is possible to apply a combination of the @throws and @catches decorators on one function. For instance the following combination is possible:

@throws(IOError) @catches(IOError) def print_lines(filename): ...

( but not the other way round because the decorators do not commute! ). Unlike Java it is not required to declare @throws on a function that bypasses a checked exception without catching in.

This use-pattern might be adapted to enable different kinds of communication between caller and callee. Otherwise there is no loss of generality in this implementation because any value can be passed using the exception mechanism.

http://java.sun.com/docs/books/tutorial/essential/exceptions/catchOrDeclare.html http://www.artima.com/intv/solid.html http://www.javaworld.com/javaworld/javaqa/2002-02/01-qa-0208-exceptional.html http://en.wikipedia.org/wiki/Exception_handling

3 comments

Dmitry Dvoinikov 17 years, 6 months ago  # | flag

Nice one. What is the reason @catches exists (other than being a reminder) ? Shouldn't @catches and @throws be merged together or @catches be totally gotten rid of ?

If there is another reason for @catches, why would one want to declaratively specify the handler as in

@catches(ZeroDivisionError, handler = lambda exc, msg, traceback: "zero-div") ?

A bit ugly to me. Also, if a method fails to catch what it's supposed to catch, isn't it the same as if it was a method with @throws throwing an unexpected one ?

Anyhow, this looks like a good companion to these related decorators:

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/426123

kay schluehr (author) 17 years, 6 months ago  # | flag

Handling what shall be handled. If you want to get rid of @catches how do you check for the exception to be caught?

The argument signature of the handler callback might be "ugly" but it is the excact complement of Pythons raise statement:

http://docs.python.org/dev/ref/raise.html

The @catches decorator serves the single purpose of guaranteeing that the declared exception is actually handled. But it doesn't change the behaviour of the program otherwise. That's why it doesn't do anything interesting by default but re-raises the caught exception ( the act of catching the exception is a proof of it's treatment. I do don't see another way to check this without doing static analysis ). You are right or course that it is not much more than an enforced reminder but these are checked exception declarations anyway.

Ori Peleg 17 years, 5 months ago  # | flag

Niiiice! I dislike checked exceptions, but this is so cool! (not going to use it though :)

Created by kay schluehr on Sat, 23 Sep 2006 (PSF)
Python recipes (4591)
kay schluehr's recipes (9)

Required Modules

Other Information and Tasks