Math 480 Lecture 6: Exception Handling, Decorators

Like many standard programming languages, Python supports exception handling, which allows you to raise and handle error conditions eloquently. 

{{{id=9| /// }}}

Below we create a function divide.  If d is zero, it just raises an exception.

{{{id=2| def divide(n, d): if d == 0: raise ZeroDivisionError, "Cannot divide by 0" return n/d /// }}} {{{id=3| divide(5, 7) /// 5/7 }}} {{{id=1| divide(5, 0) /// Traceback (most recent call last): File "", line 1, in File "_sage_input_16.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("ZGl2aWRlKDUsIDAp"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmp0ZomO8/___code___.py", line 3, in exec compile(u'divide(_sage_const_5 , _sage_const_0 )' + '\n', '', 'single') File "", line 1, in File "/tmp/tmp2Wb2yA/___code___.py", line 5, in divide raise ZeroDivisionError, "Cannot divide by 0" ZeroDivisionError: Cannot divide by 0 }}}

In fact, anytime you try to divide numbers at the denominator is 0, Sage will raise a ZeroDivisionError.  We can catch this case if we want, and return something else, as illustrated below:

{{{id=5| def divide2(n, d): try: return divide(n, d) # or just put "n/d" except ZeroDivisionError: return 'infinity' /// }}} {{{id=6| divide2(5, 3) /// 5/3 }}} {{{id=7| divide2(5, 0) /// 'infinity' }}} {{{id=13| /// }}}

This page http://docs.python.org/lib/module-exceptions.html lists all the standard builtin exceptions along with what each means. Some common exceptions that appear in mathematical programming include:  TypeError, ZeroDivisionError, ArithmeticError, ValueError, RuntimeError, NotImplementedError, OverflowError, IndexError.  We illustrate each of these below.

{{{id=19| ''.join([1,2]) /// Traceback (most recent call last): File "", line 1, in File "_sage_input_27.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("Jycuam9pbihbMSwyXSk="),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmpCL14di/___code___.py", line 3, in exec compile(u"''.join([_sage_const_1 ,_sage_const_2 ])" + '\n', '', 'single') File "", line 1, in TypeError: sequence item 0: expected string, sage.rings.integer.Integer found }}} {{{id=18| 1/0 /// Traceback (most recent call last): File "", line 1, in File "_sage_input_28.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("MS8w"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmpVKi4Rg/___code___.py", line 3, in exec compile(u'_sage_const_1 /_sage_const_0 ' + '\n', '', 'single') File "", line 1, in File "element.pyx", line 1549, in sage.structure.element.RingElement.__div__ (sage/structure/element.c:11981) File "integer.pyx", line 1661, in sage.rings.integer.Integer._div_ (sage/rings/integer.c:11944) File "integer_ring.pyx", line 286, in sage.rings.integer_ring.IntegerRing_class._div (sage/rings/integer_ring.c:5142) ZeroDivisionError: Rational division by zero }}} {{{id=20| factor(0) /// Traceback (most recent call last): File "", line 1, in File "_sage_input_29.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("ZmFjdG9yKDAp"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmpFIbfTQ/___code___.py", line 3, in exec compile(u'factor(_sage_const_0 )' + '\n', '', 'single') File "", line 1, in File "/sagenb/flask/sage-4.6.2/local/lib/python2.6/site-packages/sage/rings/arith.py", line 2226, in factor int_ = int_, verbose=verbose) File "integer.pyx", line 3278, in sage.rings.integer.Integer.factor (sage/rings/integer.c:19918) ArithmeticError: Prime factorization of 0 not defined. }}} {{{id=17| CRT(2, 1, 3, 3) /// Traceback (most recent call last): File "", line 1, in File "_sage_input_30.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("Q1JUKDIsIDEsIDMsIDMp"),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmpJtSSKC/___code___.py", line 3, in exec compile(u'CRT(_sage_const_2 , _sage_const_1 , _sage_const_3 , _sage_const_3 )' + '\n', '', 'single') File "", line 1, in File "/sagenb/flask/sage-4.6.2/local/lib/python2.6/site-packages/sage/rings/arith.py", line 2674, in crt raise ValueError("No solution to crt problem since gcd(%s,%s) does not divide %s-%s" % (m, n, a, b)) ValueError: No solution to crt problem since gcd(3,3) does not divide 2-1 }}} {{{id=15| find_root(SR(1), 0, 5) /// Traceback (most recent call last): File "", line 1, in File "_sage_input_33.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("ZmluZF9yb290KFNSKDEpLCAwLCA1KQ=="),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmp1lwODp/___code___.py", line 3, in exec compile(u'find_root(SR(_sage_const_1 ), _sage_const_0 , _sage_const_5 )' + '\n', '', 'single') File "", line 1, in File "/sagenb/flask/sage-4.6.2/local/lib/python2.6/site-packages/sage/numerical/optimize.py", line 72, in find_root return f.find_root(a=a,b=b,xtol=xtol,rtol=rtol,maxiter=maxiter,full_output=full_output) File "expression.pyx", line 7736, in sage.symbolic.expression.Expression.find_root (sage/symbolic/expression.cpp:28881) RuntimeError: no zero in the interval, since constant expression is not 0. }}} {{{id=16| RealField(50)(brun) /// Traceback (most recent call last): File "", line 1, in File "_sage_input_36.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("UmVhbEZpZWxkKDUwKShicnVuKQ=="),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmpGRRlu8/___code___.py", line 3, in exec compile(u'RealField(_sage_const_50 )(brun)' + '\n', '', 'single') File "", line 1, in File "parent.pyx", line 915, in sage.structure.parent.Parent.__call__ (sage/structure/parent.c:6668) File "coerce_maps.pyx", line 156, in sage.structure.coerce_maps.NamedConvertMap._call_ (sage/structure/coerce_maps.c:4045) File "expression.pyx", line 837, in sage.symbolic.expression.Expression._mpfr_ (sage/symbolic/expression.cpp:4949) File "expression.pyx", line 767, in sage.symbolic.expression.Expression._eval_self (sage/symbolic/expression.cpp:4717) File "pynac.pyx", line 1720, in sage.symbolic.pynac.py_eval_constant (sage/symbolic/pynac.cpp:15790) File "parent.pyx", line 915, in sage.structure.parent.Parent.__call__ (sage/structure/parent.c:6668) File "coerce_maps.pyx", line 156, in sage.structure.coerce_maps.NamedConvertMap._call_ (sage/structure/coerce_maps.c:4045) File "/sagenb/flask/sage-4.6.2/local/lib/python2.6/site-packages/sage/symbolic/constants.py", line 1291, in _mpfr_ raise NotImplementedError, "%s is only available up to %s bits"%(self.name(), self._bits) NotImplementedError: brun is only available up to 41 bits }}} {{{id=22| float(5)^float(902830982304982) /// Traceback (most recent call last): File "", line 1, in File "_sage_input_37.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("ZmxvYXQoNSleZmxvYXQoOTAyODMwOTgyMzA0OTgyKQ=="),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmpO0g_RB/___code___.py", line 3, in exec compile(u'float(_sage_const_5 )**float(_sage_const_902830982304982 )' + '\n', '', 'single') File "", line 1, in OverflowError: (34, 'Numerical result out of range') }}} {{{id=21| v = [1,2,3] v[10] /// Traceback (most recent call last): File "", line 1, in File "_sage_input_38.py", line 10, in exec compile(u'open("___code___.py","w").write("# -*- coding: utf-8 -*-\\n" + _support_.preparse_worksheet_cell(base64.b64decode("diA9IFsxLDIsM10KdlsxMF0="),globals())+"\\n"); execfile(os.path.abspath("___code___.py"))' + '\n', '', 'single') File "", line 1, in File "/tmp/tmpDki5ej/___code___.py", line 4, in exec compile(u'v[_sage_const_10 ]' + '\n', '', 'single') File "", line 1, in IndexError: list index out of range }}} {{{id=12| /// }}}

The most important points to learn about exceptions are:

  • Three keywords: try, except, raise.  Memorize this!
  • How to catch multiple possible exceptions correctly (there is a serious gotcha here!).
  • "finally"

There is of course much more to exceptions, but these are the key points.   We illustrate the last two below in a contrived example.

{{{id=24| def divide(n, d): try: return n/d except (ZeroDivisionError, ValueError), msg: print msg return '%s/%s'%(n,d) except TypeError, NotImplementedError: # the above line is PURE EVIL(!) print "NotImplementedError is now '%s'"%NotImplementedError print "What have I done?!" finally: print "The finally block is *always* executed." /// }}} {{{id=28| divide(2,3) /// The finally block is *always* executed. 2/3 }}} {{{id=8| divide(2, 0) /// Rational division by zero The finally block is *always* executed. '2/0' }}} {{{id=25| divide('hi', 'mom') /// NotImplementedError is now 'unsupported operand type(s) for /: 'str' and 'str'' What have I done?! The finally block is *always* executed. }}}

The form of the except statement is:

      except [single exception], message

or

      except (tuple,of,exceptions), message

A common and extremely confusing error, is to write

      except exception1, exception2:

The result is that if exception1 occurs, then exception2 is set to the error message.  This is very confusing.  I have wasted hours because of this.  Don't make the same mistake.

{{{id=30| /// }}}

Decorators

The definition of decorators is pretty easy.  The use of them is subtle, powerful, and potentially dangerous.

From PEP 318:

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

This is equivalent to:

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

Amazingly, that is it!

To motivate decorators, let's make a function echo that takes as input a function f, and returns a new function that acts just like f, except it prints all of its inputs.  Here we use *args and **kwds, which is something that I have not mentioned before.  In Python, you use *args to refer to all of the positional inputs to a function, and **kwds to refer to all of the keyword inputs.  When you do this, args is a tuple of the positional inputs, and kwds is a dictionary of the keyword:value pairs. 

{{{id=275| def echo(f): def g(*args, **kwds): print "args =", args print "kwds =", kwds return f(*args, **kwds) return g /// }}} {{{id=298| def add_em_up(a, b, c): return a + b + c /// }}} {{{id=299| add_em_up(1, 2, 3) /// 6 }}}

The following works, but it sort of looks funny.

{{{id=282| add_em_up = echo(add_em_up) add_em_up(1, 2, 3) /// args = (1, 2, 3) kwds = {} 6 }}}

Using a decorator right when we define add_em_up is much cleaner:

{{{id=281| @echo def add_em_up(a, b, c): return a + b + c /// }}} {{{id=284| add_em_up(1, 2, 3) /// args = (1, 2, 3) kwds = {} 6 }}}

Here's another example of a very handy decorator (only available in the notebook):

{{{id=286| @interact def add_em_up(a=1, b=2, c=3): return a + b + c ///
}}} {{{id=287| /// }}}

A hope you can sense the possibilities...  Here we do type checking.

{{{id=221| class returns: def __init__(self, typ): self._typ = typ def __call__(self, f): return lambda *args, **kwds: self._typ(f(*args, **kwds)) /// }}} {{{id=224| @returns(float) def f(n,m): """Returns n + m.""" return n + m /// }}} {{{id=290| f(2,3) /// 5.0 }}} {{{id=226| type(f(5,6)) /// }}} {{{id=303| f('4', '123') /// 4123.0 }}} {{{id=211| /// }}}

Here's another example I use all the time.  If you put @parallel(ncpus) before a function and you call the function using a list as input, then the function gets evaluated at each element of the list in parallel, and the results are returned as an iterator.   If you call the function without giving a list as input, it just works as usual (not in parallel). 

{{{id=297| @parallel(10) def f(n): sleep(1) # make function seem slow return n*(n+1)/2 /// }}} {{{id=305| %time for n in [1..10]: print n, f(n) /// 1 1 2 3 3 6 4 10 5 15 6 21 7 28 8 36 9 45 10 55 CPU time: 0.00 s, Wall time: 10.00 s }}} {{{id=293| %time for X in f([1..10]): print X /// (((1,), {}), 1) (((2,), {}), 3) (((3,), {}), 6) (((4,), {}), 10) (((5,), {}), 15) (((6,), {}), 21) (((7,), {}), 28) (((8,), {}), 36) (((9,), {}), 45) (((10,), {}), 55) CPU time: 0.19 s, Wall time: 1.32 s }}} {{{id=292| /// }}} {{{id=209| /// }}}