Exceptions are useful for more than just signalling errors. They can also be used to help you handle the error, and potentially even fix the problem (true self-healing program!).
Consider this cut down version of the .setHeight
function from the last session...
def setHeight(height):
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
print("setting the height to %s" % height)
The code currently correctly detects if the user supplies a height that is below 0 or above 2.5. However, what about when the user tries to set the height to something that is not a number?
setHeight("cat")
We get a weird error message that says we have a TypeError
, as you cannot order a string and an integer.
One way to address this is to ask that height
is converted to a float
, using height = float(height)
def setHeight(height):
height = float(height)
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
print("setting the height to %s" % height)
However, this hasn't made the error any easier to understand, as we now get a ValueError
raised...
setHeight("cat")
The solution is for us to handle the exception, using a try...except
block
def setHeight(height):
try:
height = float(height)
except:
raise TypeError("Invalid height: '%s'. You can only set the height to a numeric value" % height)
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
print("setting the height to %s" % height)
setHeight("cat")
What's happened here? The try:
line starts a try-block. The code that is in the try-block is run. If any of this code raises an exception, then execution stops in the try-block, and switches instead to the code in the except-block (everything within the except:
block). In our case, float(height)
raised an exception, so execution jumped to the except-block, in which we ran the raise TypeError(...)
code.
Now the error is much more informative, allowing the user to better understand what has gone wrong. However, exception handling can do more than this. It can allow you to fix the problem. Consider this example...
setHeight("1.8 m")
We as humans can see that this could be an acceptable input. However, the computer needs help to understand. We can add code to the except-block that can try to resolve the problem. For example, imagine we had a function that could interpret heights from strings...
def string_to_height(height):
"""This function tries to interpret the passed argument as a height
in meters. The format should be 'X m', 'X meter' or 'X meters',
where 'X' is a number
"""
# convert height to a string - this always works
height = str(height)
words = height.split(" ")
if len(words) == 2:
if words[1] == "m" or words[1] == "meter" or words[1] == "meters":
try:
return float(words[0])
except:
pass
# Getting here means that we haven't been able to extract a valid height
raise TypeError("Cannot extract a valid height from '%s'" % height)
We can now call this function from within the except-block of setHeight
def setHeight(height):
try:
height = float(height)
except:
height = string_to_height(height)
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
print("setting the height to %s" % height)
setHeight("1.8 m")
Here is a copy of the Person
class from the last session. Edit the setHeight
function so that it uses exception handling and the string_to_height
function to correctly interpret heights such as "1.8 m", and so that it gives a useful error message if it is given something weird. Check that the function correctly responds to a range of valid and invalid inputs.
class Person:
"""Class that holds a person's height"""
def __init__(self, height=0, weight=0):
"""Construct a person with the specified name, height and weight"""
self.setHeight(height)
self.setWeight(weight)
def setHeight(self, height):
"""Set the person's height in meters"""
try:
height = float(height)
except:
height = string_to_height(height)
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This shoud be between 0 and 2.5 meters" % height)
self._height = height
def setWeight(self, weight):
"""Set the person's weight in kilograms"""
if weight < 0 or weight > 500:
raise ValueError("Invalid weight: %s. This should be between 0 and 500 kilograms" % weight)
self._weight = weight
def getHeight(self):
"""Return the person's height in meters"""
return self._height
def getWeight(self):
"""Return the person's weight in kilograms"""
return self._weight
def bmi(self):
"""Return the person's body mass index (bmi)"""
if (self.getHeight() == 0 or self.getWeight() == 0):
raise NullPersonError("Cannot calculate the BMI of a person with zero "
"height or weight (%s,%s)" % (self.getHeight(),self.getWeight()))
return self.getWeight() / self.getHeight()**2
p = Person(height="cat", weight=20)
Create a string_to_weight
function that interprets weights in kilograms (e.g. "5 kg", "5 kilos" or "5 kilograms"). Now edit the Person.setWeight
function so that it uses exception handling and string_to_weight
to to correctly interpret weights such as 35.5 kg
and gives a useful error message if it is given something weird. Check that your function responds correctly to a range of valid and invalid inputs.
def string_to_weight(weight):
"""This function tries to interpret the passed argument as a weight
in kilograms. The format should be 'X kg' 'X kilogram' or 'X kilograms',
where 'X' is a number
"""
# convert weight to a string - this always works
weight = str(weight)
words = weight.split(" ")
if len(words) == 2:
if words[1] == "kg" or words[1] == "kilogram" or words[1] == "kilograms" \
or words[1] == "kilo" or words[1] == "kilos":
try:
return float(words[0])
except:
pass
# Getting here means that we haven't been able to extract a valid weight
raise TypeError("Cannot extract a valid weight from '%s'" % weight)
class Person:
"""Class that holds a person's height"""
def __init__(self, height=0, weight=0):
"""Construct a person with the specified name, height and weight"""
self.setHeight(height)
self.setWeight(weight)
def setHeight(self, height):
"""Set the person's height in meters"""
try:
height = float(height)
except:
height = string_to_height(height)
if height < 0 or height > 2.5:
raise ValueError("Invalid height: %s. This shoud be between 0 and 2.5 meters" % height)
self._height = height
def setWeight(self, weight):
"""Set the person's weight in kilograms"""
try:
weight = float(weight)
except:
weight = string_to_weight(weight)
if weight < 0 or weight > 500:
raise ValueError("Invalid weight: %s. This should be between 0 and 500 kilograms" % weight)
self._weight = weight
def getHeight(self):
"""Return the person's height in meters"""
return self._height
def getWeight(self):
"""Return the person's weight in kilograms"""
return self._weight
def bmi(self):
"""Return the person's body mass index (bmi)"""
if (self.getHeight() == 0 or self.getWeight() == 0):
raise NullPersonError("Cannot calculate the BMI of a person with zero "
"height or weight (%s,%s)" % (self.getHeight(),self.getWeight()))
return self.getWeight() / self.getHeight()**2
p = Person(weight="55.6 kilos", height="1.5 meters")