[Tutor] Another example of closures: a function that intercepts exceptions
Danny Yoo
dyoo@hkn.eecs.berkeley.edu
Thu Dec 12 15:29:05 2002
[The following post is slightly advanced, and written in a badly rushed
fashion; I'm still at work at the moment, but thought this was a cute
example to show folks.]
Hi everyone,
During yesterday's Baypiggies Python meeting, someone brought up a
question about closures: why would anyone want to use something so
academic and weird?
Here's one concrete example where they might come in handy:
######
def wrapErrorsToDefaultValue(function, default_value=None):
"""
wrapErrorsToDefaultValue(function, default_value=None)
-> wrapped function
This adds a small exception handling wrapper around a given
function. This wrapped function should behave similarly to the
input, but if an exception occurs, the wrapper intercepts and
returns the default_value instead.
"""
def new_function(*args, **kwargs):
try:
return function(*args, **kwargs)
except:
return default_value
return new_function
######
wrapErrorsToDefaultValue() is a function that is pretty cute: it takes in
a function, and returns a new function that's a mimic of the inputted
function... Except it acts as a safety net if the function dies:
###
>>> def divide(a, b):
... return a / b
...
>>> divide(3, 0)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 2, in divide
ZeroDivisionError: integer division or modulo by zero
>>>
>>> wrapped_divide = wrapErrorsToDefaultValue(divide, "This is bad!")
>>> wrapped_divide(42, 2)
21
>>> wrapped_divide(42, 0)
'This is bad!'
###
The wrapped_divide() function doesn't raise an error like the first
divide() function: rather, it returns the default value that we pass in.
A more realistic example where something like this might be useful is XML
DOM parsing: I'm finding myself diving through some XML data, but not
knowing if a certain element exists or not in the marked-up data file.
In the XML files that I'm reading, there are a list of "gene models", each
of which possibly contain a "CDS" or "PROTEIN" sequence element. (For
people who are interested, the DTD of the data format I'm parsing is:
ftp://ftp.tigr.org/pub/data/a_thaliana/ath1/BACS/CHR1/tigrxml.dtd
)
I'm using the pulldom module:
http://www.python.org/doc/lib/module-xml.dom.pulldom.html
to parse these files, just because the biological sequences involved can
get quite large. Anyway, back to my example! Here's a bit of code that
I'm using to pull the text contents of a CDS_SEQUENCE element nested in a
MODEL element:
###
def get_cds_sequence(dom_node):
"""Tries to return the coding region sequence of a model gene."""
if not dom_node.getElementsByTagName('MODEL'):
return None
first_model = dom_node.getElementsByTagName('MODEL')[0]
if not first_model.getElementsByTagName('CDS_SEQUENCE'):
return None
return first_model.getElementsByTagName('CDS_SEQUENCE')[0]\
.firstChild.data
###
I'm doing all these checks because if I'm not careful, I'll get an
IndexError. But the code feels like it's just tiptoeing around a
minefield.
There is a good solution though: I can use exception handling to make this
code less awkward:
###
def get_cds_sequence(dom_node):
"""Tries to return the coding region sequence of a model gene."""
try:
first_model = dom_node.getElementsByTagName('MODEL')[0]
return first_model.getElementsByTagName('CDS_SEQUENCE')[0]\
.firstChild.data
except IndexError:
return None
###
But it's still a slight hassle having to wrap everything in try/except
blocks. I'll be doing this for about ten of my functions, and I don't
want to wrap each function with it's own little try/except.
The code becomes even nicer, though, when I take advantage of that
wrapErrorsToDefaultValue() function:
###
def _get_cds_sequence(dom_node):
"""Tries to return the coding region sequence of a model gene."""
first_model = dom_node.getElementsByTagName('MODEL')[0]
return first_model.getElementsByTagName('CDS_SEQUENCE')[0]\
.firstChild.data
def _get_protein_sequence(dom_node):
"""Tries to return the protein sequence of a model gene."""
first_model = dom_node.getElementsByTagName('MODEL')[0]
return first_model.getElementsByTagName('PROTEIN_SEQUENCE')[0]\
.firstChild.data
for func, name in [ (_get_cds_sequence, 'getCdsSequence'),
(_get_protein_sequence, 'getProteinSequence')] :
globals()[name] = wrapErrorsToDefaultValue(func)
###
(The only problem here is that wrapErrorsToDefaultValue() is too strong of
a safety net: it should really be weakened so that it only responses to
that specific IndexError, rather than everything.)
And we can take closures even further: the code of get_cds_sequence() and
get_protein_sequence() is almost identical, so we can tease the common
elements out:
###
def make_model_seq_function(sequence_type):
"""Makes a new function for extracting the MODEL/[sequence_type]
of a model gene."""
def new_function(dom_node):
"Extracts the %s sequence of a model gene" % sequence_type
first_model = dom_node.getElementsByTagName('MODEL')[0]
return first_model.getElementsByTagName(sequence_type)[0]\
.firstChild.data
return wrapErrorsToDefaultValue(new_function)
for seq_type, name in [ ('CDS_SEQUENCE', 'getCdsSequence'),
('PROTEIN_SEQUENCE', 'getProteinSequence')]:
globals()[name] = make_model_seq_function(seq_type)
###
Anyway, I hope that made some sort of sense. Back to work for me...
*grin*