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.
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
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
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.
Niiiice! I dislike checked exceptions, but this is so cool! (not going to use it though :)