{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8d96c164-300f-4b01-907c-d645b5338b42",
   "metadata": {
    "tags": []
   },
   "source": [
    "# Object Oriented Programming\n",
    "\n",
    "\n",
    "A programming paradigm that utilizes the concepts of \"objects\" which represent related bits of data & code.\n",
    "\n",
    "A common misconception is that a language needs classes to be object-oriented.  While classes are the most common feature provided in OO-focused languages, one can write code in any language that fits this paradigm.\n",
    "\n",
    "In a hypothetical language that only had data structures and functions, we might write code like:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f1101c65-ba9b-4a87-a0cb-71cdc3c73726",
   "metadata": {},
   "outputs": [],
   "source": [
    "#costume_is_scary(person_a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4ae30b4-bd82-46ac-90bd-f6d885018f52",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "person_a = {\"name\": \"Andy\", \"costume\": \"Cowboy\", \"candy\": []}\n",
    "person_b = {\"name\": \"Gil\", \"costume\": \"Robot\", \"candy\": []}\n",
    "person_c = {\"name\": \"Lisa\", \"costume\": \"Ghost\", \"candy\": []}\n",
    "\n",
    "candy_bag = [\"Kit Kat\", \"Kit Kat\", \"Lollipop\", \"M&Ms\"]\n",
    "\n",
    "def costume_is_scary(person : dict) -> bool:\n",
    "    return person[\"costume\"] in (\"Ghost\", \"Wolfman\", \"Mummy\")\n",
    "\n",
    "def do_trick(person):\n",
    "    print(f\"{person['name']} did a trick\")\n",
    "\n",
    "def trick_or_treat(person):\n",
    "    success = give_candy(candy_bag, person)\n",
    "    # extra candy for scary costumes!\n",
    "    if costume_is_scary(person):\n",
    "        give_candy(candy_bag, person)\n",
    "    if not success:\n",
    "        do_trick(person)\n",
    "\n",
    "def give_candy(candy_bag, person):\n",
    "    if candy_bag:\n",
    "        candy = random.choice(candy_bag)\n",
    "        candy_bag.remove(candy)\n",
    "        person[\"candy\"].append(candy)\n",
    "        return True\n",
    "    else:\n",
    "        return False"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35e5ef1d-5ac3-4771-8ec7-cfeb00ee2fa9",
   "metadata": {},
   "source": [
    "A common pattern to see is a lot of functions that need a particular data structure as a parameter.  Objects give us a way to connect that code, making it more clear what should be passed in, and reducing the chance of errors.\n",
    "\n",
    "The same code might be rewritten as:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "94ddd4b7-3229-4307-b097-c0d03f5c5a73",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self, name, costume):\n",
    "        self.name = name\n",
    "        self.costume = costume\n",
    "        self.candy = []\n",
    "\n",
    "    def is_scary(self):\n",
    "        return self.costume in (\"Ghost\", \"Wolfman\", \"Mummy\")\n",
    "    \n",
    "    def do_trick(self):\n",
    "        self.tricks = True\n",
    "        print(f\"{self.name} did a trick\")\n",
    "        \n",
    "    def accept_candy(self, candy):\n",
    "        self.candy.append(candy)\n",
    "        \n",
    "class NoCandy(Exception):\n",
    "    pass\n",
    "\n",
    "class House:\n",
    "    def __init__(self, initial_candy):\n",
    "        self.candy = initial_candy\n",
    "    \n",
    "    def get_candy(self):\n",
    "        if not self.candy:\n",
    "            raise NoCandy(\"no more candy!\")\n",
    "        candy = random.choice(self.candy)\n",
    "        self.candy.remove(candy)\n",
    "        return candy\n",
    "    \n",
    "\n",
    "def trick_or_treat(person, house):\n",
    "    try:\n",
    "        candy = house.get_candy()\n",
    "        person.accept_candy(candy)\n",
    "        if person.is_scary():\n",
    "            person.accept_candy(house.get_candy())\n",
    "    except NoCandy:\n",
    "        do_trick(person, house)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b581e45-4da7-4c39-8dbd-bf55d5836441",
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person(\"James\", \"Wolfman\")\n",
    "p2 = Person(\"Fred\", \"Mummy\")\n",
    "l1 = list()\n",
    "l2 = list()\n",
    "p.is_scary()\n",
    "p.accept_candy(\"Chocolate\")\n",
    "p.candy"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50cfecf9-db6a-46a3-8441-4b4765e97324",
   "metadata": {},
   "source": [
    "This code provides blueprints for what data & actions a \"person\" has.  We also take our \"candy_bag\" list and turn it into a full-fledged object as well, since presumably we'd have multiple copies of it in our real-world application."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "78fb4860-13c9-4c90-951c-c9b19ff6f47d",
   "metadata": {},
   "source": [
    "## Terminology\n",
    "\n",
    "- **Object** - An encapsulation of data & related operations.\n",
    "- **Class** - A blueprint for an object, providing methods that will act on instances of the data.\n",
    "- **Instance** - An object created from a class \"blueprint\".\n",
    "- **Method** - A function that is tied to a specific class.\n",
    "- **Attribute** - Data that is tied to a specific instance.\n",
    "- **Constructor** - A special method that creates & populates an instance of a class.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd8d1021-89d5-4572-8fc8-7083f0414ce6",
   "metadata": {
    "tags": []
   },
   "source": [
    "## Everything in Python is an Object\n",
    "\n",
    "`isinstance` is the preferred way to check if an item is of a particular type.\n",
    "\n",
    "It can return true for multiple types, we'll see why this is the case shortly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb098ee4-813d-4ede-9fba-04caf9922a76",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "isinstance([1, 2, 3], list)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8f904840-e5f1-4f44-a11a-dcb568914b98",
   "metadata": {},
   "outputs": [],
   "source": [
    "isinstance([1, 2, 3], tuple)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b7c087da-0213-41e4-a298-873d2e677ab9",
   "metadata": {},
   "outputs": [],
   "source": [
    "isinstance([1, 2, 3], object)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1ad4e49c-dbc2-406b-9148-6f708c01f4b9",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "s = set([1,2,3])\n",
    "\n",
    "# using constructors here for demo purposes, generally would use a literal (e.g. [], 0, \"\") for these\n",
    "ll = list()  \n",
    "ll.append(str())\n",
    "ll.append(int())\n",
    "ll.append(float())\n",
    "ll.append(s)\n",
    "ll.append(print)\n",
    "\n",
    "print(ll)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ea08451b-862a-446f-beb4-13bd8fcaa869",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "[isinstance(item, object) for item in ll]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e4c2a53e-9070-4446-b72b-d54f63fe8ead",
   "metadata": {},
   "source": [
    "Keeping this in mind can help keep things straight when we delve deeper into making our own objects.\n",
    "\n",
    "Let's revisit a few things that we already know:\n",
    "\n",
    "- each `list` is independent of all others, when you create a new via `list()` (or `[]`) that is an **instance**\n",
    "- calling things like `.append` operate on the instance they are called from. \n",
    "- Some methods modify the underlying object (`.append`) while others just provide a return value like any other function.  (What are some non-modifying methods?)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e870d8fe-786d-4499-86fc-4808a89981a3",
   "metadata": {},
   "source": [
    "(Placeholder: MW class got here)\n",
    "## Classes in Python"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "350fc7e7-ded5-4d4c-bab5-59de74d23764",
   "metadata": {},
   "source": [
    "### Instances, Classes, and Instantiation\n",
    "\n",
    "One way to think of classes are as blueprints for creating specific realizations.\n",
    "\n",
    "The blueprint can specify features that vary from car to car (color, transmission type, etc.).  We can create multiple car **instances** with different values for a given attribute."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "1fce0f7d-7065-432d-8672-8fefd2b11f67",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Honda Civic 2019\n",
      "Chevy Volt 2022\n"
     ]
    }
   ],
   "source": [
    "class Car:\n",
    "    # __init__ is a special method\n",
    "    # known as a double-underscore or dunder method\n",
    "    #  in Python it represents our constructor\n",
    "\n",
    "    def __init__(self, make, model, year=2000):\n",
    "        #print(type(self))\n",
    "        self.make = make\n",
    "        self.model = model\n",
    "        self.year = year\n",
    "        self.mileage = 0\n",
    "        self.hybrid = False\n",
    "        \n",
    "# to actually create Cars, we need to call this constructor\n",
    "car1 = Car(\"Honda\", \"Civic\", 2019)\n",
    "car2 = Car(\"Chevy\", \"Volt\", 2022)\n",
    "print(car1.make, car1.model, car1.year)\n",
    "print(car2.make, car2.model, car2.year)\n",
    "car3 = car2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "8cb58e57-c4ba-4066-ab43-2c8469757918",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "car3 is car2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "18729ce3-7250-4a72-833c-de48ca03ac93",
   "metadata": {},
   "outputs": [],
   "source": [
    "car2.year += 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b8612eda-9d93-4c17-a724-e4f93064da20",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2023\n"
     ]
    }
   ],
   "source": [
    "print(car3.year)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "07f8b1f1-c2cc-4e93-98a4-0f5cbdee72f4",
   "metadata": {},
   "source": [
    "This is known as *instantiation*, making an instance of the class.\n",
    "\n",
    "### `self` & methods\n",
    "\n",
    "The first parameter of methods is always `self`.  \n",
    "\n",
    "This parameter is never passed directly, but is a local reference to the object the instance is being called upon.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "68e49035-d0cc-4f24-b19b-c84d568a1233",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class Car:\n",
    "    def __init__(self, make, model, year):\n",
    "        self.make = make\n",
    "        self.model = model\n",
    "        self.year = year\n",
    "        self.mileage = 0\n",
    "        self.hybrid = False\n",
    "        self.driver = None\n",
    "        \n",
    "    def print_report(self):\n",
    "        print(f\"{self.year} {self.make} {self.model} with {self.mileage} miles\")\n",
    "        \n",
    "    def drive(self, miles):\n",
    "        self.mileage += miles\n",
    "        \n",
    "car1 = Car(\"Honda\", \"Civic\", 2019)\n",
    "car2 = Car(\"Chevy\", \"Volt\", 2022)\n",
    "car2.mileage"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "0e2162f2-7e5a-451b-96e9-b3fd4c5bcfaa",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2019 Honda Civic with 0 miles\n"
     ]
    }
   ],
   "source": [
    "car1.print_report()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "dbb352b0-7cc4-476f-9d58-e7fff9c3ae35",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1000\n",
      "2022 Chevy Volt with 1000 miles\n"
     ]
    }
   ],
   "source": [
    "car2.drive(500)\n",
    "print(car2.mileage)\n",
    "car2.print_report()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "756e9562-79bc-4e38-9eb5-e5c0e8622124",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "car1.print_report()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4afa851b-562e-4b81-a92a-017275f7d246",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "print(car1.mileage)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fec57402-3e07-482a-b51d-0dda7e25e031",
   "metadata": {},
   "source": [
    "Because of `self`, methods can know which instance they are operating upon."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8fec9ea6-1e09-42c8-808d-0eb5952fb8e2",
   "metadata": {},
   "source": [
    "#### How does this work?\n",
    "\n",
    "This is confusing at first glance, where does `self` come from? \n",
    "\n",
    "It is actually the \"parameter before the dot\".\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0c0e0e3b-308d-411f-842d-e422d8dc7968",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# explicitly call Car.print_report and pass self\n",
    "#car2.print_report()\n",
    "Car.print_report(car2)   \n",
    "# this is not how we call class methods! (but it works)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "6f0599d6-df4f-413d-9c87-ac7ca479b35f",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[4, 4]"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ll = []\n",
    "ll.append(4)\n",
    "list.append(ll, 4) # list is class, ll is self here\n",
    "ll"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41a91dbb-868b-4cf9-92fc-a417da548e4f",
   "metadata": {},
   "source": [
    "#### What happens if `self` is omitted?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "184e8e69-0eb8-4068-bb3c-a2aec2433fec",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "class Mistake:\n",
    "    def __init__(self):\n",
    "        print(\"constructor!\")\n",
    "    \n",
    "    def method_no_self():\n",
    "        print(\"method!\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "42fb46df-2abb-4387-9e49-5ddb27d0c23a",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "constructor!\n"
     ]
    },
    {
     "ename": "TypeError",
     "evalue": "Mistake.method_no_self() takes 0 positional arguments but 1 was given",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[23], line 2\u001b[0m\n\u001b[1;32m      1\u001b[0m m \u001b[38;5;241m=\u001b[39m Mistake()\n\u001b[0;32m----> 2\u001b[0m \u001b[43mm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmethod_no_self\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m      3\u001b[0m \u001b[38;5;66;03m# rewritten as Mistake.method_no_self(m)\u001b[39;00m\n",
      "\u001b[0;31mTypeError\u001b[0m: Mistake.method_no_self() takes 0 positional arguments but 1 was given"
     ]
    }
   ],
   "source": [
    "m = Mistake()\n",
    "m.method_no_self()\n",
    "# rewritten as Mistake.method_no_self(m)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9c92e9f-65cd-4a7a-847a-6f5651552672",
   "metadata": {},
   "source": [
    "### Attributes\n",
    "\n",
    "- Created on assignment, like other variables.\n",
    "- `self.name = value`\n",
    "- All attributes are accessible from inside the class and outside:\n",
    "  - `self.name` from inside.\n",
    "  - `instance_name.name` from outside.\n",
    "  \n",
    "**Best practice: create all attributes inside constructor!**\n",
    "\n",
    "Why?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "31f1a926-7099-4740-9f86-769967fa41a9",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "my_car = Car(\"DMC\", \"DeLorean\", 1982)\n",
    "my_car.driver_name = \"Marty\" # allowed, but to be avoided\n",
    "my_car.whatever_i_want = [1, 2, 3]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8c0afa4e-0219-4b45-a527-b63263a04d5d",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(my_car.driver)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2082903-236a-4b8b-b47f-a491ca3c71d6",
   "metadata": {},
   "source": [
    "#### Exception to the rule: function objects\n",
    "\n",
    "Functions are objects, and can have attributes assigned to them as well.\n",
    "\n",
    "We sometimes do this since there's no opportunity to assign them before. (Because functions do not have constructors we can modify.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f0e0212d-363e-4bde-bfa4-3e717b800f5b",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "def f():\n",
    "    print(f\"called f()\")\n",
    "    #f.call_count = 0 # NO\n",
    "f.call_count = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b15355ff-359d-4fc8-b47a-04e4c0d4afd5",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "f.call_count += 1\n",
    "f()\n",
    "print(f.call_count)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1cbc7aa8-19b6-43eb-91bb-69b889bd2a59",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# using a decorator to add call_count to any function\n",
    "def counter(func):\n",
    "    #inner.call_count\n",
    "    def inner(*args, **kwargs):\n",
    "        inner.call_count += 1\n",
    "        print(f\"call count {inner.call_count}\")\n",
    "        return func(*args, **kwargs)\n",
    "    inner.call_count = 0\n",
    "    return inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad71f5ff-e97d-4559-8c37-c4b1af0c0ec2",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "@counter\n",
    "def f():\n",
    "    print(\"called f()\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4654848c-b793-4847-a18c-072da3188868",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "@counter\n",
    "def g():\n",
    "    print(f\"called g()\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d2e7192b-bb8c-4b55-9a38-1f9d00e650c7",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "f()\n",
    "f()\n",
    "f()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa824ffd-8301-41e3-89f3-ef474dcc3025",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "g()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b9ca97f1-7d64-4d27-a685-fa17251acf3b",
   "metadata": {},
   "source": [
    "## Encapsulation\n",
    "\n",
    "A class should be responsible for modifications to its own state.\n",
    "\n",
    "Why might it be a bad idea to allow users to change attributes?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "68b9d90d-17ed-40c2-a3f2-736d7d6357ca",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# oops!\n",
    "car2.mileage -= 100\n",
    "car2.hybrid = \"no\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28b09877-bd45-4d57-a671-9927cc02f1aa",
   "metadata": {},
   "source": [
    "Furthermore, imagine we've noticed sometimes `year` is an `int` and other times a `str`.  We could decide to remedy this in our constructor like:\n",
    "\n",
    "```python\n",
    "class Car:\n",
    "    def __init__(self, make, model, year):\n",
    "        self.make = make\n",
    "        self.model = model\n",
    "        self.year = int(year)\n",
    "        self.mileage = 0\n",
    "        self.hybrid = False\n",
    "        \n",
    "    def drive(self, miles):\n",
    "        if miles < 0:\n",
    "            return # maybe an error instead? \n",
    "        self.mileage += miles\n",
    "```\n",
    "\n",
    "We can also protect against trying to roll back the odometer by driving in reverse.\n",
    "\n",
    "If other code is assigning to internal variables, we need to make these checks/changes in dozens of places.\n",
    "\n",
    "**Encapsulation** therefore allows the implementation of a class interface to be changed with *minimal impact* upon users of the class.\n",
    "\n",
    "Good object-oriented design involves thinking through what **interface** you're providing to code making use of your objects.\n",
    "\n",
    "Python has changed how (e.g.) `dict` works internally several times over the decades.  Imagine if each time they did so, methods like `.keys()` and `pop()` stopped working."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "638405f2-8e72-4450-95e6-973ffcbee224",
   "metadata": {
    "tags": []
   },
   "source": [
    "### \"private\" in Python\n",
    "\n",
    "Some languages use access specifiers like \"private\", \"public\", \"protected\" to handle this.  Python instead relies on convention.\n",
    "\n",
    "A name with a single underscore at the front is meant to be \"internal\" to the class, and should not be modified except from methods of that class.\n",
    "\n",
    "A name with a double underscore at the front is actually modified internally by Python to avoid people assigning to it accidentally.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "41292841-0aa9-4bfd-9cfe-c12f860bbdd9",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2022 Chevy Volt with 500 miles\n"
     ]
    }
   ],
   "source": [
    "class Car: \n",
    "    def __init__(self, make, model, year):\n",
    "        self._make = make \n",
    "        self._model = model \n",
    "        self._year = year\n",
    "        self.__mileage = 0\n",
    "                \n",
    "    def drive(self, miles):\n",
    "        if miles > 0:\n",
    "            self.__mileage += miles\n",
    "        else:\n",
    "            ...\n",
    "            \n",
    "    def print_report(self):\n",
    "        print(f\"{self._year} {self._make} {self._model} with {self.__mileage} miles\")\n",
    "    \n",
    "car1 = Car(\"Honda\", \"Civic\", 2019)\n",
    "car2 = Car(\"Chevy\", \"Volt\", 2022)\n",
    "\n",
    "car2.drive(500)\n",
    "car2.print_report()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b3fcebf-571c-424b-88f2-dbb8fb029705",
   "metadata": {},
   "outputs": [],
   "source": [
    "car2._year"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "ef88e138-13c2-4174-b51f-03d1c3aad5aa",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "'Car' object has no attribute '__mileage'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[28], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m car2\u001b[38;5;241m.\u001b[39m__mileage \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m100\u001b[39m\n",
      "\u001b[0;31mAttributeError\u001b[0m: 'Car' object has no attribute '__mileage'"
     ]
    }
   ],
   "source": [
    "car2.__Car_mileage -= 100"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "d09cbac0-fba3-420c-9760-1efe24a9ce48",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['_Car__mileage',\n",
       " '__class__',\n",
       " '__delattr__',\n",
       " '__dict__',\n",
       " '__dir__',\n",
       " '__doc__',\n",
       " '__eq__',\n",
       " '__format__',\n",
       " '__ge__',\n",
       " '__getattribute__',\n",
       " '__gt__',\n",
       " '__hash__',\n",
       " '__init__',\n",
       " '__init_subclass__',\n",
       " '__le__',\n",
       " '__lt__',\n",
       " '__module__',\n",
       " '__ne__',\n",
       " '__new__',\n",
       " '__reduce__',\n",
       " '__reduce_ex__',\n",
       " '__repr__',\n",
       " '__setattr__',\n",
       " '__sizeof__',\n",
       " '__str__',\n",
       " '__subclasshook__',\n",
       " '__weakref__',\n",
       " '_make',\n",
       " '_model',\n",
       " '_year',\n",
       " 'drive',\n",
       " 'print_report']"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dir(car2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cb2ba9e0-a5af-45bd-bc21-c25bc9414637",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "car2._make = \"???\"\n",
    "print(car2._make) \n",
    "# soft protection, can still access but \"at your own risk\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4ea90f2f-6530-4732-b151-4fca1c431ab1",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(car2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "886b1b5a-d47a-4112-b37d-7117fd16e37a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<__main__.Car object at 0x103792d40>\n"
     ]
    }
   ],
   "source": [
    "carA = Car(\"Honda\", \"Civic\", 2019)\n",
    "carB = Car(\"Honda\", \"Civic\", 2019)\n",
    "carA == carB\n",
    "\n",
    "print(carA)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5320f144-0164-4f09-b191-7e5eae6c020c",
   "metadata": {},
   "source": [
    "### Dunder Methods\n",
    "\n",
    "Methods that begin and end with double-underscore are called special methods or dunder methods.  These allow us to define classes that implement existing protocols.\n",
    "\n",
    "* `__repr__`\n",
    "* `__str__`\n",
    "* `__eq__`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "id": "9647e5ad-86a4-486f-bf7e-44668d25a472",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "class Car: \n",
    "    def __init__(self, make, model, year):\n",
    "        self._make = make \n",
    "        self._model = model \n",
    "        self._year = year\n",
    "        self.__mileage = 0\n",
    "\n",
    "    def drive(self, miles):\n",
    "        if miles > 0:\n",
    "            self.__mileage += miles\n",
    "        else:\n",
    "            ...\n",
    "       \n",
    "    def __eq__(self, other):\n",
    "        # we can decide equality does/doesn't include mileage\n",
    "        return (self._make == other._make \n",
    "                and self._model == other._model \n",
    "                and self._year == other._year)\n",
    "    \n",
    "    def __repr__(self):\n",
    "        return f\"repr Car({self._make}, {self._model}, {self._year}, mileage={self.__mileage})\"\n",
    "    __str__ = __repr__\n",
    "    #def __str__(self):\n",
    "    #    return f\"str {self._year} {self._make} {self._model} with {self.__mileage} miles\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14e8fbb4-0453-425d-804e-ee2a4aa44976",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "eb77364b-b30d-4a10-b0fb-355fa83e003d",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "truck = Car(\"Ford\", \"F-150\", 1985)\n",
    "truck2 = Car(\"Ford\", \"F-150\", 1985)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "d8302542-27c1-4b1b-9bce-59c6d7ce527c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "repr Car(Ford, F-150, 1985, mileage=0)"
      ]
     },
     "execution_count": 52,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "truck"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "id": "5c124af8-9d7d-4909-9124-1ba2bb170f16",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "repr Car(Ford, F-150, 1985, mileage=0)"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def f():\n",
    "    return truck\n",
    "f()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "id": "e5b3f849-83b8-4625-b160-1141e39a1a99",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "repr Car(Ford, F-150, 1985, mileage=0)\n"
     ]
    }
   ],
   "source": [
    "print(truck)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "8f4919d6-bab6-49d4-9c73-dbf0790b118c",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'str 1985 Ford F-150 with 0 miles'"
      ]
     },
     "execution_count": 48,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "str(truck)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "74bf6a50-b18d-443d-a0ef-e2f1b8004898",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "truck == car1 # eq"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "01bdde7f-c4a8-4014-9f75-ff041370892d",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "truck.__eq__(truck2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "b29e3c72-a392-4991-aeb5-fee9463495f2",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "truck == truck2 # eq"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "26edc5e9-84ad-4e6f-9e9f-c7a546f7f7b7",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "truck is truck2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d08d5dbf-7662-4534-8a3a-d44a79ee56f0",
   "metadata": {},
   "source": [
    "### `str` vs `repr`\n",
    "\n",
    "`repr` is supposed to be a programmatic interpretation, used in debugging output. In jupyter/ipython if a function returns a value we see the repr by default.\n",
    "\n",
    "`str` is used when print is called, or an explicit conversion to string as shown above.\n",
    "\n",
    "If only `__repr__` is defined, then `str(obj)` will use `__repr__`, so if you don't have a need for them to differ, then define `__repr__`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f21be66-4b7a-4ef7-97e0-ff1c382993f6",
   "metadata": {
    "tags": []
   },
   "source": [
    "## Protocols, Duck-Typing, and Polymorphism\n",
    "\n",
    "In a language like C++, functions can be created with one name but different argument lists.\n",
    "\n",
    "```c++\n",
    "void foo(int x)\n",
    "void foo(double x)\n",
    "void foo(int x, double y)\n",
    "```\n",
    "\n",
    "The compiler can decide which function to call at compile time based on the types given.\n",
    "\n",
    "This is called \"polymorphism\".\n",
    "\n",
    "We've seen one way to achieve similar results via variadic arguments.\n",
    "\n",
    "In Python, polymorphism stems from the idea **\"the meaning of an operation depends on the objects being operated on\"**.\n",
    "\n",
    "```python\n",
    "1 + 5  # addition\n",
    "\"1\" + \"5\" # string concatenation\n",
    "[1,2,3] + [4,5] # list concatenation\n",
    "```\n",
    "\n",
    "Remember, we mentioned that everything in Python is an `object` and `object`s have operations associated with them. \n",
    "\n",
    "```python\n",
    "def times(x, y):\n",
    "     return x * y\n",
    "```\n",
    "\n",
    "As long as our objects `x` and `y` support the `*` protocol, it is safe to call `times(x, y)`.\n",
    "\n",
    "### Duck Typing\n",
    "\n",
    "In Python, instead of forcing our arguments to be specific types, we use something known as \"Duck Typing.\"  This comes from the expression:\n",
    "\"If it looks like a duck, and it quacks like a duck, it might as well be a duck.\"\n",
    "\n",
    "## Protocols\n",
    "\n",
    "Another way of thinking about this is that objects of a given type follow a certain protocol.\n",
    "\n",
    "- \"addable\"\n",
    "- \"comparable\"\n",
    "- \"iterable\"\n",
    "- \"callable\"\n",
    "\n",
    "These are typically implemented via dunder methods.  To be \"addable\" an object needs a `__add__` method at minimum.  To be comparable it needs `__eq__` and `__lt__` or `__gt__` at least.\n",
    "\n",
    "Let's look at iterable now through this new lens:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "id": "a161eedc-520a-4827-a0ba-701f1cba3376",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1\n",
      "4\n",
      "9\n",
      "16\n",
      "25\n"
     ]
    }
   ],
   "source": [
    "l = [1, 2, 3, 4, 5]\n",
    "g = (x**2 for x in l)\n",
    "r = range(8)\n",
    "\n",
    "# iterable: we can use it in a for loop\n",
    "for x in g:  # or g, or r\n",
    "    print(x)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9f73f6a7-b97d-457d-92bc-f96ee9f45246",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "g"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2caf44fa-74e1-4e42-95d3-f0a070d34dc7",
   "metadata": {},
   "source": [
    "What actually happens here?\n",
    "\n",
    "The iteration protocol has a few requirements:\n",
    "\n",
    "- When an iterable is passed to an iteration context (for loop, comprehension, `map`, etc.) The context will call `iter(iterable)` to obtain the object's iterator.  \n",
    "- `iter(obj)` calls `obj.__iter__`\n",
    "- Iterator objects return values one at a time, we can call `next(iterator)` to obtain the next value. \n",
    "`next(it)` calls `it.__next__`\n",
    "- `StopIteration` is raised when there are no more values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "id": "2616192f-8adc-45a8-a988-3558aebe1eb3",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<list_iterator at 0x1042a85e0>"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "iter([1, 2, 3])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "id": "a71a043a-1c1b-4c88-b489-b5e47b9f2d4f",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<list_iterator object at 0x1042ab6d0>\n",
      "<generator object <genexpr> at 0x104622a40>\n",
      "<range_iterator object at 0x1042abb70>\n",
      "<list_iterator object at 0x1042a9c00>\n"
     ]
    }
   ],
   "source": [
    "l = [1, 2, 3]\n",
    "g = (x**2 for x in l)\n",
    "r = range(8)\n",
    "\n",
    "li = iter(l)\n",
    "gi = iter(g)\n",
    "ri = iter(r)\n",
    "li2 = iter(l)\n",
    "\n",
    "print(li)\n",
    "print(gi)\n",
    "print(ri)\n",
    "print(li2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "id": "6aec1be0-9094-450c-95f7-91ebec9c3c76",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3"
      ]
     },
     "execution_count": 62,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "next(li)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "032cfb1f-25f6-4359-8190-53129ec1b67b",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 63,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "next(li2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dbb7148c-af4a-434b-9863-1ff7083fab05",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "#print(next(li))\n",
    "#print(next(gi))\n",
    "print(next(ri))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "id": "4260dadd-9476-4103-bf7f-496812904a1e",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "ename": "StopIteration",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mStopIteration\u001b[0m                             Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[64], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mli\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m      2\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mnext\u001b[39m(gi))\n\u001b[1;32m      3\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28mnext\u001b[39m(ri))\n",
      "\u001b[0;31mStopIteration\u001b[0m: "
     ]
    }
   ],
   "source": [
    "print(next(li))\n",
    "print(next(gi))\n",
    "print(next(ri))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "599aa900-9cc4-4579-b94a-8c0474e4ef06",
   "metadata": {
    "tags": []
   },
   "source": [
    "### Discussion\n",
    "\n",
    "- What else is iterable?\n",
    "- What are other protocols we've seen?\n",
    "- Do all iterables eventually raise `StopIteration`?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "id": "6b4cb301-3152-4cd2-bba7-6f0cdd258afe",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1\n",
      "2\n",
      "3\n",
      "4\n"
     ]
    },
    {
     "ename": "StopIteration",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mStopIteration\u001b[0m                             Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[65], line 4\u001b[0m\n\u001b[1;32m      2\u001b[0m iterator \u001b[38;5;241m=\u001b[39m \u001b[38;5;28miter\u001b[39m(iterable)\n\u001b[1;32m      3\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m----> 4\u001b[0m     \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43miterator\u001b[49m\u001b[43m)\u001b[49m)\n",
      "\u001b[0;31mStopIteration\u001b[0m: "
     ]
    }
   ],
   "source": [
    "iterable = [1, 2, 3, 4] # iterable\n",
    "iterator = iter(iterable)\n",
    "while True:\n",
    "    print(next(iterator))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f7e925d3-f7ad-4ef1-97bd-8300dbb346e7",
   "metadata": {},
   "outputs": [],
   "source": [
    "f(x[0] + y[\"test\"])\n",
    "\n",
    "f.__call__(x.__getitem__(0).__add__(y.__getitem__(\"test\")))"
   ]
  }
 ],
 "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.10.15"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
