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 "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 "The most important points to learn about exceptions are:
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| /// }}}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 ///
|
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)) ///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| /// }}}