{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Error Handling\n",
    "\n",
    "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!).\n",
    "\n",
    "Consider this cut down version of the `.setHeight` function from the last session..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def setHeight(height):\n",
    "    if height < 0 or height > 2.5:\n",
    "        raise ValueError(\"Invalid height: %s. This should be between 0 and 2.5 m\" % height)\n",
    "    print(\"setting the height to %s\" % height)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(\"cat\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We get a weird error message that says we have a `TypeError`, as you cannot order a string and an integer.\n",
    "\n",
    "One way to address this is to ask that `height` is converted to a `float`, using `height = float(height)`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def setHeight(height):\n",
    "    height = float(height)\n",
    "    \n",
    "    if height < 0 or height > 2.5:\n",
    "        raise ValueError(\"Invalid height: %s. This should be between 0 and 2.5 m\" % height)\n",
    "    print(\"setting the height to %s\" % height)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However, this hasn't made the error any easier to understand, as we now get a `ValueError` raised..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(\"cat\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The solution is for us to handle the exception, using a `try...except` block"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def setHeight(height):\n",
    "    try:\n",
    "        height = float(height)\n",
    "    except:\n",
    "        raise TypeError(\"Invalid height: '%s'. You can only set the height to a numeric value\" % height)\n",
    "    \n",
    "    if height < 0 or height > 2.5:\n",
    "        raise ValueError(\"Invalid height: %s. This should be between 0 and 2.5 m\" % height)\n",
    "    print(\"setting the height to %s\" % height)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(\"cat\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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.\n",
    "\n",
    "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..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(\"1.8 m\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def string_to_height(height):\n",
    "    \"\"\"This function tries to interpret the passed argument as a height \n",
    "       in meters. The format should be 'X m', 'X meter' or 'X meters',\n",
    "       where 'X' is a number\n",
    "    \"\"\"\n",
    "    # convert height to a string - this always works\n",
    "    height = str(height)\n",
    "        \n",
    "    words = height.split(\" \")\n",
    "            \n",
    "    if len(words) == 2:\n",
    "        if words[1] == \"m\" or words[1] == \"meter\" or words[1] == \"meters\":\n",
    "            try:\n",
    "                return float(words[0])\n",
    "            except:\n",
    "                pass\n",
    "    \n",
    "    # Getting here means that we haven't been able to extract a valid height\n",
    "    raise TypeError(\"Cannot extract a valid height from '%s'\" % height)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now call this function from within the except-block of `setHeight`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def setHeight(height):\n",
    "    try:\n",
    "        height = float(height)\n",
    "    except:\n",
    "        height = string_to_height(height)\n",
    "    \n",
    "    if height < 0 or height > 2.5:\n",
    "        raise ValueError(\"Invalid height: %s. This should be between 0 and 2.5 m\" % height)\n",
    "    print(\"setting the height to %s\" % height)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(\"1.8 m\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Exercise\n",
    "\n",
    "## Exercise 1\n",
    "\n",
    "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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Person:\n",
    "    \"\"\"Class that holds a person's height\"\"\"\n",
    "    def __init__(self, height=0, weight=0):\n",
    "        \"\"\"Construct a person with the specified name, height and weight\"\"\"\n",
    "        self.setHeight(height)\n",
    "        self.setWeight(weight)\n",
    "    \n",
    "    def setHeight(self, height):\n",
    "        \"\"\"Set the person's height in meters\"\"\"\n",
    "        if height < 0 or height > 2.5:\n",
    "            raise ValueError(\"Invalid height: %s. This shoud be between 0 and 2.5 meters\" % height)\n",
    "        self._height = height\n",
    "    \n",
    "    def setWeight(self, weight):\n",
    "        \"\"\"Set the person's weight in kilograms\"\"\"\n",
    "        if weight < 0 or weight > 500:\n",
    "            raise ValueError(\"Invalid weight: %s. This should be between 0 and 500 kilograms\" % weight)\n",
    "        self._weight = weight\n",
    "        \n",
    "    def getHeight(self):\n",
    "        \"\"\"Return the person's height in meters\"\"\"\n",
    "        return self._height\n",
    "    \n",
    "    def getWeight(self):\n",
    "        \"\"\"Return the person's weight in kilograms\"\"\"\n",
    "        return self._weight\n",
    "    \n",
    "    def bmi(self):\n",
    "        \"\"\"Return the person's body mass index (bmi)\"\"\"\n",
    "        if (self.getHeight() == 0 or self.getWeight() == 0):\n",
    "            raise NullPersonError(\"Cannot calculate the BMI of a person with zero \"\n",
    "                                  \"height or weight (%s,%s)\" % (self.getHeight(),self.getWeight()))\n",
    "            \n",
    "        return self.getWeight() / self.getHeight()**2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 2\n",
    "\n",
    "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."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
