{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "l07-c000",
   "metadata": {},
   "source": [
    "# Lecture 7 - Objects 1: classes and instances\n",
    "\n",
    "**Authors:** Fiona McNeill, Matteo Degiacomi, Rayo Verweij, and Nigel Topham\n",
    "\n",
    "## Introduction\n",
    "\n",
    "In Lecture 6, we saw how we can create our own functions. Here, we'll expand our toolkit by creating our own _types_, grouping related data and functions together in classes.\n",
    "\n",
    "### Learning outcomes\n",
    "\n",
    "In this notebook, we'll discuss:\n",
    "\n",
    "- the relationship between a class and its instances;\n",
    "- defining classes and setting initial values with `__init__`;\n",
    "- reading and changing instance attributes through methods;\n",
    "- choosing between class attributes and instance attributes;\n",
    "- deleting attributes and variables using `del`; and\n",
    "- using `__str__` to give an object a readable description.\n",
    "\n",
    "### Using the notebook\n",
    "\n",
    "Remember, this notebook is _yours_! You can edit each cell, for example by adding new examples or changing values to see what happens. Feel free to experiment!\n",
    "\n",
    "A common approach for learning how to read and write code in a structured way is called **PRIMM**:\n",
    "1. **Predict**: before running the code, look at it - do you understand each line? What do you think will happen?\n",
    "2. **Run**: now run the code and see what output you get.\n",
    "3. **Investigate**: did it go as you thought it would? If not, can you figure out why?\n",
    "4. **Modify**: now change the code and once again try to predict the output. If you got it wrong the first time, did you get it right now?\n",
    "5. **Make**: finally, try to write your own program from scratch using the skills you learned.\n",
    "\n",
    "You can apply steps 1-4 to each code cell in the notebook and many will have prediction prompts to help you get started. In addition, there will be challenges that ask you to put everything together yourself and execute step 5. There will often be multiple ways to solve these, but each challenge has a potential answer listed.\n",
    "\n",
    "### Navigating the notebook\n",
    "\n",
    "A Jupyter notebook can be operated with your keyboard. Here is a **cheat sheet**:\n",
    "\n",
    "- To run the selected cell and move to the next cell, press <kbd>&#x21E7; Shift</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To run the selected cell and keep focus there, press <kbd>⌃ Ctrl</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To create a cell under the one currently highlighted, press <kbd>B</kbd>;\n",
    "- To delete the currently highlighted cell, press <kbd>D</kbd> twice (be careful with this one!) or press <kbd>X</kbd> to cut it;\n",
    "- To get help for a specific function, place the cursor within the function's brackets, hold <kbd>&#x21E7; Shift</kbd>, and press <kbd>&#x21E5; Tab</kbd>.\n",
    "\n",
    "Watch out: **code cells remember what happened in cells before**. So, especially in the more complicated notebooks later down the line, make sure to always run **every** cell from top to bottom, as one might rely on a piece of code that came before it! If your results become confusing, use **Kernel > Restart Kernel** and then rerun all the completed cells in order.\n",
    "\n",
    "### Further reading and practice\n",
    "\n",
    "- Official Python Tutorial: [A first look at classes](https://docs.python.org/3/tutorial/classes.html#a-first-look-at-classes). Inheritance will be covered in Lecture 16.\n",
    "- W3Schools: [OOP](https://www.w3schools.com/python/python_oop.asp) - all sections up to and including [Class methods](https://www.w3schools.com/python/python_class_methods.asp).\n",
    "- Helsinki Python MOOC: [Classes and objects](https://programming-26.mooc.fi/part-8/2-classes-and-objects), [Defining classes](https://programming-26.mooc.fi/part-8/3-defining-classes), and [Defining methods](https://programming-26.mooc.fi/part-8/4-defining-methods). Type annotations, which the MOOC uses, will be covered in Lecture 14, so you can leave those out of your own practice for now."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dec71dff",
   "metadata": {},
   "source": [
    "## 1. Functions and methods"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0eef8967",
   "metadata": {},
   "source": [
    "In the previous lecture, we discussed **functions**, reusable pieces of code that can receive arguments and process them to return values.\n",
    "\n",
    "The functions we discussed there, such as `print()` and `len()`, were _standalone_. In addition, however, we have already seen a few **methods**: functions that are associated with a particular object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7ffe87f",
   "metadata": {},
   "outputs": [],
   "source": [
    "up = \"hello\".upper()\n",
    "clean = \"  with some spaces    \".strip()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99c02b78",
   "metadata": {},
   "source": [
    "Which methods you can use with a particular object depends on the _type_ of that object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ffebdb6c",
   "metadata": {},
   "outputs": [],
   "source": [
    "num = 3\n",
    "nonsense = num.upper() # Throws AttributeError"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e7736dc",
   "metadata": {},
   "source": [
    "The above piece of code throws `AttributeError: 'int' object has no attribute 'upper'`. In other words, we cannot call the method `.upper()` here as it is not defined for objects of the `int` type - it would only work for strings.\n",
    "\n",
    "We can call several methods one after another, as long as each method returns an object that has the next method. This functionality is called **method chaining**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5df34e61",
   "metadata": {},
   "outputs": [],
   "source": [
    "name = input(\"What is your name? \").strip().upper().append(\"What?\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48d88b14",
   "metadata": {},
   "source": [
    "A chain of methods is evaluated from left to right. In the above piece of code, then:\n",
    "\n",
    "1. `input()` is evaluated first and returns a string when the user has given their input (e.g. `\"  Max\"`); next,\n",
    "2. `strip()` takes this string and removes any whitespace at the start and end, returning a second string (e.g. `\"Max\"`); then,\n",
    "3. `upper()` takes this second string and transforms it to uppercase letters (e.g. `\"MAX\"`); and finally,\n",
    "4. `append()` takes this third string and throws an `AttributeError`, because the `.append()` method only works on objects of type `list` and not of type `str`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e1d9d45",
   "metadata": {},
   "source": [
    "## 2. Classes and instances"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b52892f2",
   "metadata": {},
   "source": [
    "In Python, every piece of data that we use is called an **object**. _Object_ is just a generic term for a data structure; as discussed, each object has a particular **type**, such as string, integer, float, list, dict, or set. Which type an object has determines:\n",
    "\n",
    "1. The **value(s)** it can hold: for strings, a piece of text; for integers whole numbers; for lists, many different objects indexed by integers; etc.; as well as\n",
    "2. The **methods** that are available for use to manipulate the object, such as `.upper()` for strings.\n",
    "\n",
    "The _specification_ of a type is called a **class**. Objects that follow this specification are called **instances** of the class.\n",
    "\n",
    "For example, the object `[\"hello\", \"world\"]` is an instance of the `list` class. It contains two other objects, `\"hello\"` and `\"world\"`, which are both instances of the `str` class.\n",
    "\n",
    "Saying that something \"is of type `x`\" and \"is an instance of the `x` class\" are different ways of saying the same thing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "96f80f13",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(type([\"hello\", \"world\"]))\n",
    "print(type(\"hello\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1fe650bf",
   "metadata": {},
   "source": [
    "This finally explains why the `type()` function produces output that says `<class 'x'>`! `type()` returns the class that its argument is an instance of, and `print()` displays it in that form.\n",
    "\n",
    "Enough theory. Let's see how we can define our _own_ classes now!\n",
    "\n",
    "**Predict:** Read the following code. Can you figure out how it works? What do you think the two print statements will show?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14fd13c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Five:\n",
    "    n = 5\n",
    "\n",
    "new_five = Five()\n",
    "\n",
    "print(new_five.n)\n",
    "print(type(new_five))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f51cc92",
   "metadata": {},
   "source": [
    "Classes are defined with the `class` keyword, followed by its name and a colon. You then create an instance of a class by using its name with parentheses, just like calling a function.\n",
    "\n",
    "Class names are usually capitalised; this is not a hard rule, just a convention used in the Python community to make them stand out from functions as their syntax is so similar.\n",
    "\n",
    "The **class body** (indented like every body in Python!) is where we define the data and methods associated with that class.\n",
    "\n",
    "An **attribute** is a piece of data associated with a class. In the above example, our `Five` class has an attribute named `n` with a value of `5`. This is now a part of every `Five` object that we create, such as `new_five`, and can be accessed on these objects using a `.`.\n",
    "\n",
    "`type(new_five)` tells us that our object is an instance of the class `'__main__.Five'`. `__main__` is the name that Python automatically gives to the space that we are working in; more on this in Lecture 11. For now, it's enough that you can see the `Five` class represented here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "36859324",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(Five)\n",
    "print(new_five)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d7129bff",
   "metadata": {},
   "source": [
    "Printing out the class itself gives the same result as printing the `type()` of an instance of a class.\n",
    "\n",
    "Meanwhile, printing out the instance tells us again what class it is an instance of, as well as the _location in memory_ that Python has saved the object at (the long number). This isn't particularly useful for us and we won't bother with it in this course.\n",
    "\n",
    "Watch out for the parentheses: `great_number = Five` and `great_number = Five()` are not the same thing!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "544bcb30-6dcd-4d74-9f6e-93c4e36d5a46",
   "metadata": {},
   "outputs": [],
   "source": [
    "great_number = Five\n",
    "print(great_number)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "280d4848-8709-403a-b1df-d42a0ef2ba32",
   "metadata": {},
   "source": [
    "In the cell above we do _not_ create a new object. We just give the class itself a second name. If you print `great_number`, you'll see `<class '__main__.Five'>` rather than `<__main__.Five object at ...>`, which is a quick way to spot this mistake."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58e1f981-6f4c-4886-936c-df90fc42567d",
   "metadata": {},
   "source": [
    "Our `Five` class isn't particularly useful. Every instance of it starts out exactly the same, with `n` set to `5`. How can we give objects of a particular class _their own data_?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa84aabf",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Dog:\n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "\n",
    "\n",
    "first_dog = Dog(\"Luna\", 4)\n",
    "second_dog = Dog(\"Milo\", 2)\n",
    "\n",
    "print(first_dog.name, first_dog.age)\n",
    "print(second_dog.name, second_dog.age)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "344ff861",
   "metadata": {},
   "source": [
    "In our class body, in addition to defining attributes, we can define _methods_. Because methods are just functions that are associated with a particular object, they are created exactly like functions are, including using the `def` keyword, and allow you to use default arguments and docstrings.\n",
    "\n",
    "Each class comes with a special method called `__init__`, which runs when a new instance is created. This method is called the **constructor** and the process of creating a new object is called **instantiation**. It allows us to add the information that is particular to an object. The first parameter of `__init__`, usually called `self`, _refers to the single instance that the method is working with_.\n",
    "\n",
    "Inside the method's body, `self.name = name` says, _Take the value of the `name` argument and assign it to the `name` attribute of the `Dog` that is being created right now._\n",
    "\n",
    "**Predict:** We have given `__init__` a docstring. What will happen when you place your cursor inside the brackets of either call to `GoodDog()`, hold <kbd>&#x21E7; Shift</kbd> and press <kbd>&#x21E5; Tab</kbd>? (*Note: in VS Code, hovering over the call has the same effect.*)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "845781d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "class GoodDog:\n",
    "    description = \"A good boi!\"\n",
    "    \n",
    "    def __init__(self, name, age=0):\n",
    "        '''Give each dog a name and age. Default to age 0 for newborn dogs.'''\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "\n",
    "\n",
    "third_dog = GoodDog(\"Kai\", 5)\n",
    "fourth_dog = GoodDog(\"Muppet\")\n",
    "\n",
    "print(third_dog.name, third_dog.age, third_dog.description)\n",
    "print(fourth_dog.name, fourth_dog.age, fourth_dog.description)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "681ce063",
   "metadata": {},
   "source": [
    "As you can see, classes can have two types of attributes:\n",
    "\n",
    "1. **Class attributes** are shared between all instances of a class. They are defined directly in the class body, in the above example, `description`.\n",
    "2. **Instance attributes** are local to one instance of a class. They are usually defined in the `__init__` method, in the above example, `name` and `age`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-ex-book",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: model a book</b>\n",
    "\n",
    "Define a `Book` class with `title`, `author`, and `on_loan` instance attributes. New books should start with `on_loan` set to `False` by default. Create two independent instances and print their titles.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c007",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your code here!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c008",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details>\n",
    "<summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "class Book:\n",
    "    def __init__(self, title, author, on_loan=False):\n",
    "        self.title = title\n",
    "        self.author = author\n",
    "        self.on_loan = on_loan\n",
    "\n",
    "first_book = Book(\"Kindred\", \"Octavia E. Butler\")\n",
    "second_book = Book(\"The Left Hand of Darkness\", \"Ursula K. Le Guin\")\n",
    "\n",
    "print(first_book.title)\n",
    "print(second_book.title)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ee5cb3d4",
   "metadata": {},
   "source": [
    "## 3. Methods and changing state"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c009",
   "metadata": {},
   "source": [
    "The `self` parameter allows you to use instance attributes throughout multiple methods of a class.\n",
    "\n",
    "When you call a method, Python passes along the object that is written before the `.` as the first argument in the call. Therefore, **all** methods defined inside of a class body should get `self` as their first parameter.\n",
    "\n",
    "For example, in the code below, when calling `left.increment()`, `left` becomes the value of `self` inside of the class's `increment()` method.\n",
    "\n",
    "**Predict:** What will each counter's value be after the calls? What about when you add `left.reset()` before the print statement?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c010",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Counter:\n",
    "    def __init__(self):\n",
    "        self.value = 0\n",
    "\n",
    "    def increment(self):\n",
    "        self.value += 1\n",
    "\n",
    "    def reset(self):\n",
    "        self.value = 0\n",
    "\n",
    "left = Counter()\n",
    "right = Counter()\n",
    "left.increment()\n",
    "left.increment()\n",
    "right.increment()\n",
    "print(left.value, right.value)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-watch-methods",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-warning\">\n",
    "<b>Watch out: common mistakes with methods</b>\n",
    "\n",
    "- **Forgetting `self.`** Inside a method, a name without `self.` is just a local variable that disappears when the method ends (remember variable scope from Lecture 6?). Writing `value += 1` in `increment()` raises an `UnboundLocalError`. Writing `name = name` in `__init__` is sneakier: there is no error until you later read `first_dog.name` and get `AttributeError: 'Dog' object has no attribute 'name'`.\n",
    "\n",
    "- **Forgetting the parentheses.** In the example above, `left.increment` without `()` does not call the method, it only looks it up. No error is raised and `left.value` doesn't change. If it's the last line of a cell, Jupyter shows something like `<bound method Counter.increment of ...>`, which is your clue that the method was never called.\n",
    "\n",
    "- **Writing `_init_` instead of `__init__`.** The constructor needs two underscores on each side. Otherwise it's just an ordinary method that never gets called, and creating an object fails with `TypeError: Dog() takes no arguments`. Without an `__init__`, Python falls back on a default one that accepts no arguments.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c011",
   "metadata": {},
   "source": [
    "Once again, methods are just functions! As a result, they can receive other arguments and return results as well. Below, `deposit()` changes the account's balance, while `can_afford()` simply returns a Boolean without changing anything.\n",
    "\n",
    "**Predict:** What is the account's balance after the deposit? What will `can_afford(70)` return?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c012",
   "metadata": {},
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    def __init__(self, owner, balance):\n",
    "        self.owner = owner\n",
    "        self.balance = balance\n",
    "\n",
    "    def deposit(self, amount):\n",
    "        self.balance += amount\n",
    "\n",
    "    def can_afford(self, amount):\n",
    "        return self.balance >= amount\n",
    "\n",
    "account = BankAccount(\"Sam\", 50)\n",
    "account.deposit(25)\n",
    "print(account.balance)\n",
    "print(account.can_afford(70))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9d1a6ca9",
   "metadata": {},
   "source": [
    "In the `GoodDog` class, we saw how we can set default values for attributes using default arguments in the `__init__` method. You can also set values directly in the method itself:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce9b6ba3",
   "metadata": {},
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    def __init__(self, owner, balance):\n",
    "        self.owner = owner\n",
    "        self.balance = balance\n",
    "        self.locked = False\n",
    "\n",
    "    def deposit(self, amount):\n",
    "        self.balance += amount\n",
    "\n",
    "    def can_afford(self, amount):\n",
    "        return self.balance >= amount\n",
    "\n",
    "    def lock(self):\n",
    "        self.locked = True\n",
    "        \n",
    "\n",
    "account = BankAccount(\"Bobby\", 1000000)\n",
    "account.lock()\n",
    "print(account.locked)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c013",
   "metadata": {},
   "source": [
    "Have a think: **what is the difference** between setting an attribute in the body of `__init__` versus using a default argument?\n",
    "\n",
    "<details>\n",
    "<summary><b>Click for the answer</b></summary>\n",
    "\n",
    "The difference lies in what you're able to do when instantiating the class. When calling `BankAccount`, `owner` and `balance` are the _only_ possible arguments we can give it and _every_ `BankAccount` that we create will start with `locked = False`.\n",
    "\n",
    "However, in the `GoodBoy` class, the argument was optional. While `GoodBoy` objects would default to `age = 0`, we had the option to supply a different age during instantiation.\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b1cce1d",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: borrowing books</b>\n",
    "\n",
    "Let's extend the `Book` class we had above. First, change it so that all books _have to_ start with `on_loan = False`, as books cannot be entered into the catalogue when borrowed. Then, add `borrow()` and `return_book()` methods that both change `on_loan`.\n",
    "\n",
    "Instantiate a book and demonstrate both transitions.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c014",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Copy over your Book class from before and update it\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c015",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details>\n",
    "<summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "class Book:\n",
    "    def __init__(self, title, author):\n",
    "        self.title = title\n",
    "        self.author = author\n",
    "        self.on_loan = False\n",
    "\n",
    "    def borrow(self):\n",
    "        self.on_loan = True\n",
    "\n",
    "    def return_book(self):\n",
    "        self.on_loan = False\n",
    "\n",
    "book = Book(\"Kindred\", \"Octavia E. Butler\")\n",
    "book.borrow()\n",
    "print(book.on_loan)\n",
    "book.return_book()\n",
    "print(book.on_loan)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bdb33dcf",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\">\n",
    "<b>Note: <code>self</code>-ishness</b>\n",
    "\n",
    "Technically, `self` does not have to be called `self` - you can name the parameter anything you want. Python will just supply the method's instance to its first parameter. It's not recommended to call it anything other than `self`, however, to make it clear that it is a \"special\" parameter.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35c3fe0c",
   "metadata": {},
   "source": [
    "## 4. Updating and deleting attributes"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "967506f9",
   "metadata": {},
   "source": [
    "You can update attributes directly outside of classes.\n",
    "\n",
    "**Predict:** What happens when you try to update the class attribute of an instance?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "583bfd5f",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Student:\n",
    "    institution = \"University of Edinburgh\"\n",
    "\n",
    "    def __init__(self, name, year):\n",
    "        self.name = name\n",
    "        self.year = year\n",
    "\n",
    "aisha = Student(\"Aisha\", 1)\n",
    "ben = Student(\"Ben\", 2)\n",
    "\n",
    "print(Student.institution)\n",
    "print(aisha.institution, ben.institution)\n",
    "print(aisha.year, ben.year)\n",
    "\n",
    "ben.year = 3\n",
    "aisha.institution = \"Heriot-Watt University\"\n",
    "\n",
    "print(Student.institution)\n",
    "print(aisha.institution, ben.institution)\n",
    "print(aisha.year, ben.year)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f786d5b",
   "metadata": {},
   "source": [
    "Objects can have instance and class attributes of the _same name_, and the instance attribute will take precedence over the class attribute.\n",
    "\n",
    "In the above example, when updating `aisha.institution`, it does _not_ change the `institution` class attribute. Instead, it creates a _new_ instance attribute on the `aisha` object, set to `\"Heriot-Watt University\"`, that is accessed when we print `aisha.institution` as the instance attribute takes precedence.\n",
    "\n",
    "To update the class attribute, you'd have to access it through the class name:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b94ee863",
   "metadata": {},
   "outputs": [],
   "source": [
    "Student.institution = \"University of Glasgow\"\n",
    "print(Student.institution, aisha.institution, ben.institution)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46fbec90",
   "metadata": {},
   "source": [
    "Of course, `aisha.institution` still prints `\"Heriot-Watt University\"`, because even though the class attribute has changed, the instance attribute still exists and that one takes precedence.\n",
    "\n",
    "If we want to remove the instance attribute, we'd have to **delete** it using the `del` keyword."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "518913b0",
   "metadata": {},
   "outputs": [],
   "source": [
    "del aisha.institution\n",
    "print(Student.institution, aisha.institution, ben.institution)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4bbafc5c",
   "metadata": {},
   "source": [
    "Now the instance attribute has been removed and only the class attribute remains.\n",
    "\n",
    "`del` works for both types of attributes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7d427f7e",
   "metadata": {},
   "outputs": [],
   "source": [
    "del Student.institution\n",
    "print(Student.institution, aisha.institution, ben.institution) # Throws an AttributeError as the attribute no longer exists!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a5aaa524",
   "metadata": {},
   "source": [
    "In fact, `del` works not only for attributes, but for variables as well:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "445bb740",
   "metadata": {},
   "outputs": [],
   "source": [
    "fib = [1, 1, 2, 3, 5]\n",
    "print(fib)\n",
    "\n",
    "del fib\n",
    "print(fib) # Throws a NameError, as `fib` is not defined anymore!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5bb4b0d",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\">\n",
    "<b>Note: what does <code>del</code> actually do?</b>\n",
    "\n",
    "`del` removes a _name_, not the object itself. If another variable still refers to the same object (remember aliasing from Lecture 5?), the object survives and can still be used through that other name.\n",
    "\n",
    "Be careful with `del`! When you don't need a variable anymore, it's often easier to just leave it alone. It can be useful, however, to delete unwanted instance attributes like in the example above.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c018",
   "metadata": {},
   "source": [
    "### Mutable attributes\n",
    "\n",
    "It wouldn't be much of a Python lecture if we did not briefly touch upon mutability, now would it?\n",
    "\n",
    "**Predict:** Which playlist will contain `\"Resurrections\"`?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c019",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Playlist:\n",
    "    category = \"Personal\"\n",
    "\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        self.tracks = []\n",
    "\n",
    "    def add_track(self, track):\n",
    "        self.tracks.append(track)\n",
    "\n",
    "focus = Playlist(\"Focus\")\n",
    "travel = Playlist(\"Travel\")\n",
    "focus.add_track(\"Resurrections\")\n",
    "\n",
    "print(focus.tracks)\n",
    "print(travel.tracks)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c020-prose",
   "metadata": {},
   "source": [
    "In the above class, defining `tracks` as an instance attribute makes sure that each instance of the class has its _own_ playlist. If we had set it as a class attribute, the list would be shared between all instances - and because lists are mutable, adding a track would then have added it to each instance's playlist."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c020",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: choosing attribute locations</b>\n",
    "\n",
    "Define a `Bicycle` class with class attribute `wheels = 2` and instance attributes `model` (string), `distance` (int), and `visited` (list). Add a `ride(km)` method that accumulates distance as well as a `visit(location)` method that adds `location` to the `visited` list for each bike.\n",
    "\n",
    "Next, use the class to define both a regular bike as well as one with `model = \"Tricycle\"` that has _three_ wheels. Visit some locations with both and print the journeys. Finally, update the tricycle to be a normal bike again, with two wheels, and print this number.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c021",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Create your bicycles\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c022",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details>\n",
    "<summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "class Bicycle:\n",
    "    wheels = 2\n",
    "\n",
    "    def __init__(self, model):\n",
    "        self.model = model\n",
    "        self.distance = 0\n",
    "        self.visited = []\n",
    "\n",
    "    def ride(self, km):\n",
    "        self.distance += km\n",
    "\n",
    "    def visit(self, location):\n",
    "        self.visited.append(location)\n",
    "\n",
    "city_bike = Bicycle(\"City\")\n",
    "city_bike.visit(\"Musselburgh\")\n",
    "\n",
    "special_bike = Bicycle(\"Tricycle\")\n",
    "special_bike.wheels = 3\n",
    "special_bike.visit(\"Dalkeith\")\n",
    "\n",
    "print(city_bike.visited)\n",
    "print(special_bike.visited)\n",
    "\n",
    "special_bike.model = \"Regular :(\"\n",
    "del special_bike.wheels\n",
    "print(special_bike.wheels)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fe637b1",
   "metadata": {},
   "source": [
    "## 5. Describing an object with `__str__`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c023",
   "metadata": {},
   "source": [
    "When we tried printing `new_five` in Section 2, it wasn't a particularly useful print statement: it gave us the object's type and its location in memory, and that was about it.\n",
    "\n",
    "As it turns out, Python actually lets us choose what happens when `print()` or `str()` is called on our classes' instances!\n",
    "\n",
    "We do this using another special method called `__str__` which has to return a string.\n",
    "\n",
    "Because of their special nature, `__init__` and `__str__` are both called **magic methods** or **dunder methods**, _dunder_ being short for _Double UNDERscore_. Python has many more of them, but these two are all we need for now."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c024",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Student:\n",
    "    institution = \"University of Edinburgh\"\n",
    "\n",
    "    def __init__(self, name, year):\n",
    "        self.name = name\n",
    "        self.year = year\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"{self.name}, year {self.year}\"\n",
    "\n",
    "student = Student(\"Aisha\", 1)\n",
    "print(student)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c025",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: describing a book</b>\n",
    "\n",
    "Let's finish our earlier `Book` class by implementing `__str__`. Printing an available book should produce `Title by Author (available)` and printing a borrowed one should produce `Title by Author (on loan)`.\n",
    "\n",
    "Create a book, print it, borrow it, and print it again.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-c026",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Copy over your Book class from before and update it\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-c027",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details>\n",
    "<summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "class Book:\n",
    "    def __init__(self, title, author):\n",
    "        self.title = title\n",
    "        self.author = author\n",
    "        self.on_loan = False\n",
    "\n",
    "    def borrow(self):\n",
    "        self.on_loan = True\n",
    "\n",
    "    def return_book(self):\n",
    "        self.on_loan = False\n",
    "\n",
    "    def __str__(self):\n",
    "        if self.on_loan:\n",
    "            status = \"on loan\"\n",
    "        else:\n",
    "            status = \"available\"\n",
    "        return f\"{self.title} by {self.author} ({status})\"\n",
    "\n",
    "book = Book(\"Kindred\", \"Octavia E. Butler\")\n",
    "print(book)\n",
    "book.borrow()\n",
    "print(book)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-consolidation-prompt",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "## Consolidation challenge\n",
    "\n",
    "Let's create one more class from scratch.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: logging temperatures</b>\n",
    "\n",
    "Create a `TemperatureReading` class. Each instance should have its own `station` and a `degrees` value and the class should have a shared `unit` attribute set to `\"°C\"`.\n",
    "\n",
    "Add three methods:\n",
    "\n",
    "- `fahrenheit()` returns the temperature converted to Fahrenheit;\n",
    "- `adjust(amount)` changes the degrees value by the given amount; and\n",
    "- `__str__()` returns a description containing the station and temperature in both units.\n",
    "\n",
    "Create readings for two stations, adjust one, and print both. Make sure that adjusting one station's readings leaves the other's unchanged.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l07-consolidation-work",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your solution here!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-consolidation-solution",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "class TemperatureReading:\n",
    "    unit = \"°C\"\n",
    "\n",
    "    def __init__(self, station, degrees):\n",
    "        self.station = station\n",
    "        self.degrees = degrees\n",
    "\n",
    "    def fahrenheit(self):\n",
    "        return self.degrees * 9 / 5 + 32\n",
    "\n",
    "    def adjust(self, amount):\n",
    "        self.degrees += amount\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"{self.station}: {self.degrees:.1f}{self.unit} / {self.fahrenheit():.1f}°F\"\n",
    "\n",
    "calton = TemperatureReading(\"Calton\", 12.5)\n",
    "blackford = TemperatureReading(\"Blackford\", 11.0)\n",
    "calton.adjust(0.5)\n",
    "\n",
    "print(calton)\n",
    "print(blackford)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l07-consolidation-summary",
   "metadata": {
    "tags": [
     "summary"
    ]
   },
   "source": [
    "## Summary\n",
    "\n",
    "You have now defined your own classes, created instances, and written methods to work with their attributes. When reading a class, keep track of what belongs to each instance and what belongs to the class. We will build on this in Lecture 16.\n",
    "\n",
    "### Key terms\n",
    "\n",
    "<dl>\n",
    "\t<dt><b>Object</b></dt>\n",
    "\t<dd>A data structure that has a value and methods associated with it.</dd>\n",
    "\t<dt><b>Class</b></dt>\n",
    "\t<dd>A definition of a type of object, including its data and behaviour.</dd>\n",
    "\t<dt><b>Instance</b></dt>\n",
    "\t<dd>A particular object belonging to a class.</dd>\n",
    "\t<dt><b>Instantiation</b></dt>\n",
    "\t<dd>Creating an instance of a class.</dd>\n",
    "\t<dt><b>Constructor</b></dt>\n",
    "\t<dd>The method that handles instantiation; in Python, <code>__init__</code>.</dd>\n",
    "\t<dt><b>Attribute</b></dt>\n",
    "\t<dd>A named value associated with an object or a class, accessed using a dot <code>.</code>.</dd>\n",
    "\t<dt><b>Instance attribute</b></dt>\n",
    "\t<dd>An attribute stored on an individual instance.</dd>\n",
    "\t<dt><b>Class attribute</b></dt>\n",
    "\t<dd>An attribute stored on the class, available to instances which do not have their own attribute of that name.</dd>\n",
    "\t<dt><b>Method</b></dt>\n",
    "\t<dd>A function defined in a class. An instance method receives the instance as its first argument, with the parameter usually named <code>self</code>.</dd>\n",
    "\t<dt><b>Magic</b> or <b>dunder methods</b></dt>\n",
    "\t<dd>Special methods available to classes denoted using double underscores, including <code>__init__</code> and <code>__str__</code>.</dd>\n",
    "</dl>\n",
    "\n",
    "### New syntax\n",
    "\n",
    "<dl>\n",
    "\t<dt><code>class Name:</code></dt>\n",
    "\t<dd>Begins a class definition with an indented body. By convention, class names are capitalised.</dd>\n",
    "\t<dt><code>def __init__(self, ...):</code></dt>\n",
    "\t<dd>Defines the method which initialises a newly created instance.</dd>\n",
    "\t<dt><code>self</code></dt>\n",
    "\t<dd>The conventional name for the first parameter of an instance method, referring to the instance receiving the call.</dd>\n",
    "\t<dt><code>self.attribute = value</code></dt>\n",
    "\t<dd>Stores an attribute on the current instance.</dd>\n",
    "\t<dt><code>instance = ClassName(arguments)</code></dt>\n",
    "\t<dd>Creates and initialises an instance of the class.</dd>\n",
    "\t<dt><code>instance.attribute</code></dt>\n",
    "\t<dd>Accesses an attribute using dot notation.</dd>\n",
    "\t<dt><code>instance.method(arguments)</code></dt>\n",
    "\t<dd>Calls a method, passing <code>instance</code> as the first argument of <code>.method()</code>.</dd>\n",
    "\t<dt><code>def __str__(self):</code></dt>\n",
    "\t<dd>Defines the method which returns a readable string representation of an instance.</dd>\n",
    "\t<dt><code>del</code></dt>\n",
    "\t<dd>Removes a name, such as a variable or an attribute. The object it referred to survives if another name still refers to it.</dd>\n",
    "</dl>\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
