{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Exceptions\n",
    "\n",
    "Mistakes and errors happen in computer programs as much as in real life. Like life, how you handle an error in your program shows your level of professionalism, and gives others evidence that they can trust that you have written a program that will work well.\n",
    "\n",
    "In the last section we indicated errors in the `Person.setHeight` function by printing a message to the screen and returning `False` to indicate that the call to `setHeight` had failed."
   ]
  },
  {
   "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):\n",
    "        \"\"\"Construct a person who has zero height\"\"\"\n",
    "        self._height = 0\n",
    "    \n",
    "    def setHeight(self, height):\n",
    "        \"\"\"Set the person's height to 'height', returning whether or \n",
    "           not the height was set successfully\n",
    "        \"\"\"\n",
    "        if height < 0 or height > 300:\n",
    "            print(\"This is an invalid height! %s\" % height)\n",
    "            return False\n",
    "        else:\n",
    "            self._height = height\n",
    "            return True\n",
    "        \n",
    "    def getHeight(self):\n",
    "        \"\"\"Return the person's height\"\"\"\n",
    "        return self._height"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.setHeight(-20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is not a good way of indicating an error. The issues with this are;\n",
    "\n",
    "* How does the person calling `getHeight` know to check whether the call returns `True` or `False`\n",
    "* What if we wanted to return something else? Should we return the error state and the value we want together?\n",
    "* If the error state is not checked, and nobody reads the error message printed to the screen, then the program is broken, as the person has been created with a height of 0.\n",
    "\n",
    "The solution is to send something to the programmer that they cannot ignore, which indicates that there is an error. That something is called an \"exception\".\n",
    "\n",
    "Take a look at this simple code that sets the height..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def setHeight(height):\n",
    "    if height < 0 or height > 300:\n",
    "        raise ValueError(\"Invalid height: %s. This should be between 0 and 300\" % height)\n",
    "        \n",
    "    print(\"Height is set to %s\" % height)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(-5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When we try to use an invalid value for the height, we raise (or throw) a `ValueError` exception. This stops the function from continuing, and gives us a very helpful print out of what went wrong, and where.\n",
    "\n",
    "`ValueError` is just a class. The name of the class provides us with useful information (there was an error with a value in the program). You choose what error you want to raise. Python provides a set of usefully named error classes that you can use:\n",
    "\n",
    "* `IOError` : Error raised when you have a problem with IO, e.g. opening or closing files\n",
    "* `ZeroDivisionError` : Error raised when you divide by zero\n",
    "* `TypeError` : Error raised when you are using the wrong type, e.g. maybe setting the height to a string\n",
    "* `IndexError` : Error raised when you are using an invalid index to access a list or other similar container\n",
    "* `KeyError` : Error raised when you are using an invalid key to access a dictionary or other similar container\n",
    "\n",
    "A full list of standard Python exceptions is [available here](https://docs.python.org/3/library/exceptions.html).\n",
    "\n",
    "You are free to raise any exception class you want. It is your job as a programmer to choose the one that is most sensible, e.g."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def setHeight(height):\n",
    "    if height < 0 or height > 300:\n",
    "        raise ZeroDivisionError(\"Invalid height: %s. This should be between 0 and 300\" % height)\n",
    "        \n",
    "    print(\"Height is set to %s\" % height)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(400)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using a `ZeroDivisionError` is a bad choice, as the error has nothing to do with division by zero. A `ValueError` is the right choice as the error relates to an invalid value passed to the function.\n",
    "\n",
    "You are free to create your own exception classes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class InvalidHeightError(Exception):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def setHeight(height):\n",
    "    if height < 0 or height > 300:\n",
    "        raise InvalidHeightError(\"Invalid height: %s. This should be between 0 and 300\" % height)\n",
    "        \n",
    "    print(\"Height is set to %s\" % height)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "setHeight(-10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Your own exception classes must be declared as derived from type `Exception`, hence why you have to write `class InvalidHeightError(Exception):`. As the class doesn't need to do anything else, you can use `pass` to say that nothing else needs to be added. Note that you can call your error class anything you want. By convention, it is good to end the class name with `Error` so that other programmers know what it is for."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Exercise\n",
    "\n",
    "Here is an extended copy of the `Person` code from above."
   ]
  },
  {
   "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",
    "        self._height = height\n",
    "    \n",
    "    def setWeight(self, weight):\n",
    "        \"\"\"Set the person's weight in kilograms\"\"\"\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",
    "        return self.getWeight() / self.getHeight()**2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 1\n",
    "\n",
    "Edit the above copy of `Person` to ensure that the `.setWeight` function only accepts valid weights. A valid weight is any number that is between 0 and 500 kilograms. You should raise a `ValueError` if the weight is outside this range. For the moment, do not worry about the user supplying a non-numeric weight.\n",
    "\n",
    "Also edit the above copy of `Person` to ensure that the `.setHeight` function only accepts valid heights. A valid height is any number that is between 0 and 2.5 meters. You should raise a `ValueError` if the height is outside this range. For the moment, do not worry about the user supplying a non-numeric height.\n",
    "\n",
    "Check that a `ValueError` exception is correctly raised if invalid heights or weights are supplied. Also check that the `ValueError` exception is not raised if a valid height and weight are supplied."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 2\n",
    "\n",
    "If you run the following code;\n",
    "\n",
    "```python\n",
    "p = Person()\n",
    "p.bmi()\n",
    "```\n",
    "\n",
    "it will raise a `DivideByZero` exception. This is because the calculation involves dividing by the height squared, which is zero in a default-constructed `Person`. While an exception has been raised, it is not very intuitive for another programmer to debug. A solution is to create your own named exception that provides more information.\n",
    "\n",
    "Create a new exception called `NullPersonError`, and edit the `.bmi()` function so that this exception is raised if it is called on a `Person` whose height or weight is zero.\n",
    "\n",
    "Check that the `NullPersonError` exception is raised if `.bmi()` is called on a default-constructed `Person`. Check that this exception is not raised if `.bmi()` is called on a properly constructed `Person`."
   ]
  },
  {
   "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
}
