from IPython.display import HTML
HTML(open("../include/notes.css", "r").read())
allowExceptions = True # set to `False` for Jupyter "Run All" to work
Here's a simple function known as the "Ackermann" (technically "Ackermann–Péter") function. It comes from computability theory and was proposed in 1928. We've added an optional trace
argument so we can see how it's called.
def ackermann(m, n, trace=False):
if m == 0:
result = n+1
elif m > 0 and n == 0:
result = ackermann(m-1, 1, trace)
else:
assert m > 0 and n >= 0
result = ackermann(m-1, ackermann(m, n-1, trace), trace)
if trace:
print("ackermann({}, {}) -> {}".format(m, n, result))
return result
Looks simple. Let's try it.
print(ackermann(0, 1))
print(ackermann(1, 0))
print(ackermann(2, 0))
Now let's trace the result:
print(ackermann(2, 0, True)) # False->True to trace the result
Simple, huh? So let's try m
of 4.
print(ackermann(4, 0))
A lot more recursion, but no real problems. Now, let's try n
of 1.
(Warning: If you try this at home, be sure not to set trace
to True
.)
Stand back! This may take a while!
if allowExceptions:
print(ackermann(4, 1))
What's going on here? Looks like we can only compute some values of ackermann(m,n)
, but we may not know which ones before run time. How can we prevent the program from blowing up like this?
try
... except
Statement¶try:
y = 3
raise OverflowError()
except (SyntaxError, RuntimeError, OverflowError):
print("exception detected")
raise
print("that's all")
def arbitraryFunction():
raise SyntaxError
try:
y = 3
arbitraryFunction()
except (SyntaxError, OverflowError):
print("exception detected")
raise
print("that's all")
arbitraryFunction()
syntax (simplified):
try:
{trySuite}
except {exceptionName(s)}:
{exceptionSuite}
{trySuite}
is a suite of code that may cause an exception to be raised.
{exceptionName(s)}
is an exception name or tuple of exception names.
{exceptionSuite}
is a suite of code that will be executed when one
of the exceptions is raised.
So let's use this to "wrap" an ackermann()
call in a testing framework.
def testAckermann(m, n):
try:
print("ackermann({}, {}) = {}".format(m, n, ackermann(m, n)))
except RecursionError:
print("ackermann({}, {}) = ?".format(m, n))
testAckermann(4, 1)
Exercise for the astute: Write a function checkForRuntimeError(func, *args, **kwargs)
that will test any function func()
(passing *args
and **kwargs
to it) and return True
iff it does not cause a RuntimeError
to be raised.
Uncaught exceptions cause the program to exit with lots of information about where the exception happened. Often, that's what you want to have happen.
Only catch an exception when you want to do one or more of the following:
treat the condition as something other than a bug
recover from the condition and let the program proceed
(This is especially useful for testing.)
provide more information about the bug than the default
raise
Statement¶In the above, the interpreter raised the RuntimeError
exception itself, but our code can raise exceptions explicitly with the raise
statement:
raise {exceptionName}
We'll use this in an example below.
We can pass an object {messageObject}
as a message to the exception (constructor):
raise {exceptionName}({messageObject})
and if we have
raise
all by itself in an exception handler, the current exception will be passed up the calling stack to the next higher handler. This is like "throw;
" in a C++ exception handler and something much more complicated in a Java exception handler.
try
... finally
Clause¶syntax:
try:
{trySuite}
finally:
{finallySuite}
{finallySuite}
is executed whenever {trySuite}
passes out of the calling hierarchy, including:
a normal (un-"exception"-al) end
a break
(from a surrounding loop)
a continue
(from a surrounding loop)
a return
(from the current function)
an exception (even matching the same try
)
assert
Statement¶Think of assert
as a "sanity check" for your code. It raises the AssertionError
exception. The syntax is:
assert {condition}[, {expression}]
If {condition}
is false, the exception is raised and {expression}
is used as the error message. Note: {expression}
can be any object, including a tuple
of multiple values.
def fact(n):
assert n >= 0, 'negative argument'
if n == 0:
return 1
else:
return n * fact(n-1)
print(fact(-300))
Remember the Interval
class? What happens to Interval.__div__(self, other)
when other
spanned 0? What happens when we compare two intervals that overlap?
Stand back: It's time to use an Exception
!
class IntervalRangeError(Exception):
pass
class IntervalComparisonError(Exception):
pass
class Interval:
def __init__(self, min=0, max=None):
if max == None:
max = min
if min > max:
raise IntervalRangeError
self.min = min
self.max = max
def __add__(self, y):
if not isinstance(y, Interval):
raise TypeError
return Interval(self.min+y.min, self.max+y.max)
def __lt__(self, y):
if not isinstance(y, Interval):
raise TypeError
if self.max < y.min:
return True
elif y.max < self.min:
return False
else:
raise IntervalComparisonError
def __sub__(self, y):
if not isinstance(y, Interval):
raise TypeError
return Interval(self.min-y.min, self.max-y.max)
def __pow__(self, expo):
if expo > 0:
return Interval(self.min**expo, self.max**expo)
else:
# use division to do the negative power, in case of zero-spanning
return Interval(1,1)/Interval(self.min**(-expo), self.min**(-expo))
def __mul__(self, y):
if not isinstance(y, Interval):
raise TypeError
extrema = [self.min*y.min, self.min*y.max,
self.max*y.min, self.max*y.max]
return Interval(min(extrema), max(extrema))
def __truediv__(self, y):
if not isinstance(y, Interval):
raise TypeError
if y.min <= 0 <= y.max:
raise ZeroDivisionError
extrema = [self.min/y.min, self.min/y.max,
self.max/y.min, self.max/y.max]
return Interval(min(extrema), max(extrema))
def __repr__(self):
return "[%f..%f]" % (self.min, self.max)
if __name__ == '__main__':
a = Interval(0.1, 0.25)
b = Interval(0.125, 0.185)
c = Interval(3)
d = Interval(-1,1)
print('Test of the interval arithmetic module:')
print(' a =', a)
print(' b =', b)
print(' c =', c)
print(' d =', d)
print(' a+b =', a+b)
print(' a-b =', a-b)
print(' a*b =', a*b)
print(' a/b =', a/b)
print(' a**2 =', a**2)
print(' a**(-0.5) =', a**(-0.5))
print('(a*b)/(a+b) =', (a*b)/(a+b))
try:
e = Interval(1)/d
except ZeroDivisionError:
print("division by interval containing zero was caught")
try:
if a < c:
print("less-than comparison works")
if c > a:
print("greater-than comparison works")
if a < d:
pass # should trigger exception
except IntervalComparisonError:
print("interval comparison exception handled")
Exceptions are classes, and like classes they can be arranged in a hierarchy. When parent and child exceptions have distinct exception suites, Python will always choose the most specific (i.e., the child).
We can also give exceptions special methods like __repr__()
that Python will use when needed.
class IntervalError(Exception):
pass
class IntervalRangeError(IntervalError):
pass
class IntervalComparisonError(IntervalError):
def __init__(self):
pass
def __repr__(self):
return 'IntervalComparisonError class __repr__() called'
class Interval(object):
def __init__(self, min=0, max=None):
if max == None:
max = min
if min > max:
raise IntervalRangeError
self.min = min
self.max = max
def __add__(self, y):
if not isinstance(y, Interval):
raise TypeError
return Interval(self.min+y.min, self.max+y.max)
def __lt__(self, y):
if not isinstance(y, Interval):
raise TypeError
if self.max < y.min:
return True
elif y.max < self.min:
return False
else:
raise IntervalComparisonError
def __sub__(self, y):
if not isinstance(y, Interval):
raise TypeError
return Interval(self.min-y.min, self.max-y.max)
def __pow__(self, expo):
if expo > 0:
return Interval(self.min**expo, self.max**expo)
else:
# use division to do the negative power, in case of zero-spanning
return Interval(1,1)/Interval(self.min**(-expo), self.max**(-expo))
def __mul__(self, y):
if not isinstance(y, Interval):
raise TypeError
extrema = [self.min*y.min, self.min*y.max,
self.max*y.min, self.max*y.max]
return Interval(min(extrema), max(extrema))
def __truediv__(self, y):
if type(y) == int:
y = Interval(y, 1)
if not isinstance(y, Interval):
raise TypeError
if y.min <= 0 <= y.max:
raise ZeroDivisionError
extrema = [self.min/y.min, self.min/y.max,
self.max/y.min, self.max/y.max]
return Interval(min(extrema), max(extrema))
def __repr__(self):
return "[%f..%f]" % (self.min, self.max)
a = Interval(0.1, 0.25)
b = Interval(0.125, 0.185)
c = Interval(3)
print('Test of the interval arithmetic module:')
print(' a =', a)
print(' b =', b)
print(' c =', c)
print(' a+b =', a+b)
print(' a-b =', a-b)
print(' a*b =', a*b)
print(' a/b =', a/b)
print(' a**2 =', a**2)
print(' a**(-0.5) =', a**(-0.5))
print('(a*b)/(a+b) =', (a*b)/(a+b))
print()
d = Interval(-1,1)
try:
e = Interval(1)/d
except ZeroDivisionError:
print("division by interval containing zero was caught")
print()
try:
Interval(1, -1)
except IntervalRangeError:
print("IntervalRangeError exception caught")
print()
try:
if a < c:
print("less-than comparison works")
if c > a:
print("greater-than comparison works")
if a < d:
pass # should trigger exception
except IntervalComparisonError:
if 0:
print("interval comparison exception re-raised")
raise
print()
try:
if a < d:
pass # should trigger exception
except IntervalError:
print("interval comparison superclass exception handled")
madlibs
¶Here's a piece of code that is, in the classic sense of "Pythonic", too silly. Suppose we wanted to create an on-line version of the party game "madlibs", where users provide specific instances of generic words like "adjective" or "name".
text = """
{adjective0}ly {adjective1} Sir {name} went forth from {town}.
He was not at all {adjective2} to {verb0},
O {adjective0} Sir {name}.
He was not at all {adjective2} to be {verb1}ed in {adjective3} ways,
{adjective0}, {adjective0}, {adjective0}, {adjective0} Sir {name}!
"""[1:]
# The user might enter these in an HTML form...
userEntries = {
'name': 'Nigel',
'adjective0': 'bumbling',
'adjective1': 'clumsy',
'adjective2': 'foolish',
'adjective3': 'silly',
'town': 'Issaquah',
'verb0': 'croak',
'verb1': 'smash'
}
print(text.format(**userEntries), end='')
But there's a problem here: Words that begin a sentence (e.g. on the 1st and 5th lines) need to be capitalized, but left alone elsewhere. We can get around this by overloading the __format__()
built-in for a Word
class we create. This means we can capture the formatSpec
in "{keyword:formatSpec}
" entries that str.format()
detects and use it to transform the (here, str
) object however we like.
class Word(str):
def __format__(self, formatSpec):
result = self
if len(self) > 0:
if formatSpec == 'cap':
result = self[0].upper() + self[1:]
else:
result = super(str, self).__format__(formatSpec)
return result
text = """
{adjective0:cap}ly {adjective1} Sir {name} went forth from {town}.
He was not at all {adjective2} to {verb0},
O {adjective0} Sir {name}.
He was not at all {adjective2} to be {verb1}ed in {adjective3} ways,
{adjective0:cap}, {adjective0}, {adjective0}, {adjective0} Sir {name}!
"""[1:]
# The user might enter these in an HTML form...
userEntries = {
'name': Word('Nigel'),
'adjective0': Word('bumbling'),
'adjective1': Word('clumsy'),
'adjective2': Word('foolish'),
'adjective3': Word('silly'),
'town': Word('Ellensburg'),
'verb0': Word('croak'),
'verb1': Word('smash')
}
print(text.format(**userEntries), end='')
Useful, but we can do better. Instead of adding special handling for the "cap
" formatSpec
, let's take advantage of the str.capitalize()
(or str.
anythingElse()
) builtin method.
class Word(str):
def __format__(self, formatSpec):
if not formatSpec: # formatSpec defaulted
return self
try:
# Try to look up the `formatSpec` by name as a string
# method in the dictionary of the str class and apply
# it to ``self``.
return str.__dict__[formatSpec](self)
except KeyError:
# `formatSpec` is not a known method, so do conventional
# string formatting.
return super().__format__(formatSpec)
# We're using different keywords here to show how you can choose
# them from the original quote and substitute directly.
text = """
{brave:capitalize}ly {bold} Sir {robin:capitalize} went forth from {camelot:capitalize}.
He was not {afraid} to {die},
O {brave} Sir {robin:capitalize}!
He was not at all {afraid} to be {kill}ed in {nasty} ways,
{brave:capitalize}, {brave}, {brave}, {brave} Sir {robin:capitalize}!
"""[1:]
# The user might enter these in an HTML form...
userEntries = {
'robin': Word('nigel'),
'brave': Word('brash'),
'bold': Word('heroic'),
'afraid': Word('afeared'),
'nasty': Word('ugly'),
'camelot': Word('kennewick'),
'die': Word('croak'),
'kill': Word('trash'),
}
print(text.format(**userEntries), end='')
with
Statement¶This is the last Python statement we will cover.
syntax:
with {contextManager}:
{withSuite}
A "context manager" is an instance of a class with these special methods:
__init__(self, *args, **kwds)
called when a {contextManager}
is created (as always). It takes a self
and any other arguments you care to pass.
__enter__(self)
called when a {contextManager}
is encountered in the with
statement
__exit__(self, type, value, traceback)
called when {withSuite}
passes out of the calling hierarchy, just like the
{finallySuite}
in a "try ... finally
" statement, which is actually the
main point of using a with
statement.
Here's an example of a useful context manager:
class XmlTag:
def __init__(self, tag, **attrs):
self.tag = tag
self.attrs = attrs
def __enter__(self):
if self.attrs:
print('<{}'.format(self.tag), end=' ')
for name in self.attrs:
print('{}="{}"'.format(name, self.attrs[name]), end=' ')
print('>')
else:
print('<{}>'.format(self.tag))
def __exit__(self, type, value, traceback):
print('</{}>'.format(self.tag))
with XmlTag("a", link="http://www.tricity.wsu.edu", style="x"):
print("bold text")
if 0: # enable to see recovery from exception
raise SyntaxError
# Use variables to store contexts that are reused (and help readability).
rowTag = XmlTag('tr') # use this to start table rows
datumTag = XmlTag('td') # use this to start columns within rows
boldTag = XmlTag('b') # use this to bold-face output
with XmlTag('html'):
with XmlTag('body'):
with XmlTag('h1'): # header 1 style in HTML
print("Hello, Worlds!")
print("An Extraterrestrial's Guide to the Solar System.")
with XmlTag('table', border=10, rules='all'):
with rowTag: # column header tags
with datumTag:
with boldTag:
print("planet")
with datumTag, boldTag:
print("type")
with datumTag, boldTag:
print("comments")
with rowTag:
with datumTag:
print("mercury")
with datumTag:
print("terrestrial")
if 0: # enable to see what "with" does for us
raise RuntimeError # suppose
with datumTag:
print("restaurants serve good food, but have no atmosphere")
with rowTag:
with datumTag:
print("venus")
with datumTag:
print("terrestrial")
with datumTag:
print("OK, if you don't mind 462 deg C")
with rowTag:
# alternate syntax
with datumTag: print("earth")
with datumTag: print("terrestrial (duh!)")
with datumTag: print("too touristy")
with rowTag:
with datumTag:
print("mars")
with datumTag:
print("terrestrial")
with datumTag:
print("don't let those NASA robots see you!")
with rowTag:
with datumTag:
print("jupiter")
with datumTag:
print("gas giant")
with datumTag:
print("nice for you methane breathers")
with rowTag:
with datumTag:
print("saturn")
with datumTag:
print("gas giant")
with datumTag:
print("has a familiar ring to it")
with rowTag:
with datumTag:
print("uranus")
with datumTag:
print("gas giant")
with datumTag:
print("planet of the japes")
with rowTag:
with datumTag:
print("neptune")
with datumTag:
print("gas giant")
with datumTag:
print("the outermost planet")
with boldTag:
print("now!")
The Built-in Warning Classes
The warnings
Module
The traceback
Module
We'll just mention these in passing. More details are in the book.