{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Special Methods\n",
    "\n",
    "\"Special methods\" is a technical term referring to methods that get called automatically.  In Python, they usually begin and end with double underscores.\n",
    "\n",
    "We've already seen one special method, the constructor: `__init__`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class C:\n",
    "    def __init__(self):\n",
    "        print(\"I'm inside __init__\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It's special, because it will run whenever an object is created, even though it is never explicitly called (note that `__init__` doesn't appear in the following snippet, even though clearly it runs):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "I'm inside __init__\n"
     ]
    }
   ],
   "source": [
    "obj = C()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll cover the following special methods here:\n",
    "\n",
    "* for strings: `__str__`, `__repr__`, `_repr_html_`\n",
    "* for comparison: `__eq__`, `__lt__`\n",
    "* for sequences: `__len__`, `__getitem__`\n",
    "* for context managers: `__enter__`, `__exit__`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Strings\n",
    "\n",
    "When we print or view an object, it must be automatically converted to a string first.  There are two ways.\n",
    "\n",
    "For example, let's create a datetime object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime \n",
    "today = datetime.datetime.now()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`print` automatically converts it to a string, as you can see:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2021-01-25 15:23:02.067837\n",
      "2021-01-25 15:23:02.067837\n"
     ]
    }
   ],
   "source": [
    "print(today)\n",
    "print(str(today))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we make it the last line in a cell, it gets converted to a string too, but differently:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "datetime.datetime(2021, 1, 25, 15, 23, 2, 67837)"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "today"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The idea behind two styles is that there are both non-programmers (who probably prefer the former) and programmers (who might prefer the latter) in the world.  Programmers like the latter format because it can be copy/pasted to create new objects:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "obj = datetime.datetime(2021, 1, 25, 14, 15, 50, 951625)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Python deals with these two styles by giving every object two special methods: `__repr__` and `__str__`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "For coders: datetime.datetime(2021, 1, 25, 15, 23, 2, 67837)\n",
      "For others: 2021-01-25 15:23:02.067837\n"
     ]
    }
   ],
   "source": [
    "print(\"For coders:\", today.__repr__())\n",
    "print(\"For others:\", today.__str__())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Immediately above, we're calling the special methods explicitly; they're still special because sometimes they get called explicitly (e.g., `print(today)` earlier called `__str__`).\n",
    "\n",
    "Let's say we have a class for email messages.  We can implement the special methods ourselves."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "WARNING: did you forgot to attach a file?\n"
     ]
    }
   ],
   "source": [
    "class Email:\n",
    "    def __init__(self, to, frm, subject, message, attachment=None):\n",
    "        self.to = to\n",
    "        self.frm = frm\n",
    "        self.subject = subject\n",
    "        self.message = message\n",
    "        self.attachment = attachment\n",
    "        if self.attachment == None and \"attached\" in message:\n",
    "            print(\"WARNING: did you forgot to attach a file?\")\n",
    "            \n",
    "    def __str__(self):\n",
    "        return f\"TO: {self.to}\\nFROM: {self.frm}\\nSUBJECT: {self.subject}\\n\\n{self.message}\\n\\nATTACHMENT: {self.attachment}\"\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f\"Email({repr(self.to)}, {repr(self.frm)}, {repr(self.subject)}, {repr(self.message)}, {repr(self.attachment)})\"\n",
    "\n",
    "em = Email(\"jobs@example.com\", \"somebody@gmail.com\", \"please hire me!\", \"Please see attached resume\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Email('jobs@example.com', 'somebody@gmail.com', 'please hire me!', 'Please see attached resume', None)"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# implicit call to __repr__\n",
    "em"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TO: jobs@example.com\n",
      "FROM: somebody@gmail.com\n",
      "SUBJECT: please hire me!\n",
      "\n",
      "Please see attached resume\n",
      "\n",
      "ATTACHMENT: None\n"
     ]
    }
   ],
   "source": [
    "# implicit call to __str__\n",
    "print(em)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Comparison\n",
    "\n",
    "We normally use `==` to tell if two objects are the same.  We need to do a little more work for this to work with our own types:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s1 = \"hi\"\n",
    "s2 = \"hi\"\n",
    "s1 == s2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "e1 = Email('jobs@example.com', 'somebody@gmail.com', 'please hire me!', 'Thanks!', None)\n",
    "e2 = Email('jobs@example.com', 'somebody@gmail.com', 'please hire me!', 'Thanks!', None)\n",
    "e1 == e2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We need to implement `__eq__`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Email:\n",
    "    def __init__(self, to, frm, subject, message, attachment=None):\n",
    "        self.to = to\n",
    "        self.frm = frm\n",
    "        self.subject = subject\n",
    "        self.message = message\n",
    "        self.attachment = attachment\n",
    "        if self.attachment == None and \"attached\" in message:\n",
    "            print(\"WARNING: did you forgot to attach a file?\")\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"TO: {self.to}\\nFROM: {self.frm}\\nSUBJECT: {self.subject}\\n\\n{self.message}\\n\\nATTACHMENT: {self.attachment}\"\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f\"Email({repr(self.to)}, {repr(self.frm)}, {repr(self.subject)}, {repr(self.message)}, {repr(self.attachment)})\"\n",
    "\n",
    "    def __eq__(self, other):\n",
    "        if self.to == other.to and self.frm == self.frm and self.subject == other.subject:\n",
    "            return True\n",
    "        return False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True\n",
      "False\n"
     ]
    }
   ],
   "source": [
    "e1 = Email('jobs@example.com', 'somebody@gmail.com', 'please hire me!', 'Thanks!', None)\n",
    "e2 = Email('jobs@example.com', 'somebody@gmail.com', 'please hire me!', 'Thanks!', None)\n",
    "e3 = Email('returns@example.com', 'somebody@gmail.com', 'refund please?', 'Thanks!', None)\n",
    "print(e1 == e2)\n",
    "print(e1 == e3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `__eq__` should check all the important attributes and return True when they are all equivalent (and False otherwise).  The above `__eq__` is not very complete.  It doesn't check `message` for example, which leads to strange behavior:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "e1 = Email('jobs@example.com', 'somebody@gmail.com', 'please hire me!', 'I will work hard', None)\n",
    "e2 = Email('jobs@example.com', 'somebody@gmail.com', 'please hire me!', 'I promise', None)\n",
    "e1 == e2 # should be False, but won't be because our __eq__ doesn't check message"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's say you have a class for grocery inventory, and you want to sort a list of inventory objects based on value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Inventory:\n",
    "    def __init__(self, item, amount, price):\n",
    "        self.item = item\n",
    "        self.amount = amount\n",
    "        self.price = price\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f\"Inventory({repr(self.item)}, {self.amount}, {self.price})\"\n",
    "    \n",
    "grocery = [Inventory(\"apples\", 10, 0.3), Inventory(\"oranges\", 2, 0.5), Inventory(\"kiwis\", 9, 0.2)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "'<' not supported between instances of 'Inventory' and 'Inventory'\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    grocery.sort()\n",
    "except Exception as e:\n",
    "    print(e)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As the above error suggests, we need to implement \"<\" (less-than, or lt for short) to do sorting:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[Inventory('oranges', 2, 0.5),\n",
       " Inventory('kiwis', 9, 0.2),\n",
       " Inventory('apples', 10, 0.3)]"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "class Inventory:\n",
    "    def __init__(self, item, amount, price):\n",
    "        self.item = item\n",
    "        self.amount = amount\n",
    "        self.price = price\n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f\"Inventory({repr(self.item)}, {self.amount}, {self.price})\"    \n",
    "\n",
    "    def __lt__(self, other):\n",
    "        return self.amount*self.price < other.amount*other.price\n",
    "\n",
    "grocery = [Inventory(\"apples\", 10, 0.3), Inventory(\"oranges\", 2, 0.5), Inventory(\"kiwis\", 9, 0.2)]\n",
    "grocery.sort()\n",
    "grocery"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The items are sorted from least valuable (1 dollar) to most valuable (3 dollars)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Sequences\n",
    "\n",
    "How do brackets work?  `obj[lookup]` is backed by a special function (called `__getitem__`) that takes `lookup` and returns a value.  Let's create a Sentence class that lets you grab a word in a sentence."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Sentence:\n",
    "    def __init__(self, s):\n",
    "        self.s = s\n",
    "\n",
    "    def __getitem__(self, lookup):\n",
    "        print(\"calling __getitem__ with \" + str(lookup))\n",
    "        return self.s.split()[lookup]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calling __getitem__ with 3\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'fox'"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s = Sentence(\"The quick brown fox jumps over the lazy dog\")\n",
    "s[3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we want, we can get clever and take other types.  For example, we could interpret the float 3.2 to mean we want the 2nd letter from the 3rd word (counting both from the 0th position, of course)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Sentence:\n",
    "    def __init__(self, s):\n",
    "        self.s = s\n",
    "        \n",
    "    def __getitem__(self, lookup):\n",
    "        print(\"calling __getitem__ with \" + str(lookup))\n",
    "        word_idx = int(lookup)\n",
    "        word = self.s.split()[word_idx]\n",
    "        if type(lookup) == int:\n",
    "            return word\n",
    "        letter_idx = int(round(10*(lookup - word_idx)))\n",
    "        return word[letter_idx]\n",
    "    \n",
    "    def __len__(self):\n",
    "        return len(self.s.split())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calling __getitem__ with 3.2\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'x'"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s = Sentence(\"The quick brown fox jumps over the lazy dog\")\n",
    "s[3.2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The type checks means the old behavior works too:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calling __getitem__ with 3\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'fox'"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s[3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You might have noticed we implemented `__len__` above too.  It does what you might guess:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "9"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One last thing: for loops work.  Python starts at index 0, then keeps counting up until there is an exception (which is hidden.  Check it out (noticing that no word was returned when index 9 was attempted):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calling __getitem__ with 0\n",
      "The\n",
      "calling __getitem__ with 1\n",
      "quick\n",
      "calling __getitem__ with 2\n",
      "brown\n",
      "calling __getitem__ with 3\n",
      "fox\n",
      "calling __getitem__ with 4\n",
      "jumps\n",
      "calling __getitem__ with 5\n",
      "over\n",
      "calling __getitem__ with 6\n",
      "the\n",
      "calling __getitem__ with 7\n",
      "lazy\n",
      "calling __getitem__ with 8\n",
      "dog\n",
      "calling __getitem__ with 9\n"
     ]
    }
   ],
   "source": [
    "for w in s:\n",
    "    print(w)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Context Managers\n",
    "\n",
    "Context managers work with the \"with\" statement in Python.  They're useful for making sure some code runs before and after a block, even if there is an exception."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "class TimeMe:\n",
    "    def __enter__(self):\n",
    "        print(\"start timer\")\n",
    "        self.t0 = time.time()\n",
    "    \n",
    "    def __exit__(self, exc_type, exc_value, traceback):\n",
    "        print(\"stop timer\")\n",
    "        self.t1 = time.time()\n",
    "        \n",
    "    def total(self):\n",
    "        return self.t1 - self.t0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "start timer\n",
      "stop timer\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "0.0012068748474121094"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tm = TimeMe()\n",
    "\n",
    "with tm:\n",
    "    total = 1\n",
    "    for i in range(1000):\n",
    "        total *= (i+1)\n",
    "        \n",
    "tm.total()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "start timer\n",
      "stop timer\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "2.6177990436553955"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with tm:\n",
    "    total = 1\n",
    "    for i in range(100000):\n",
    "        total *= (i+1)\n",
    "        \n",
    "tm.total()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "file objects are context managers.  This is very useful, as `__exit__` automatically closes files.  Without context managers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "f = open(\"hi.txt\", \"w\")\n",
    "f.write(\"hello\")\n",
    "f.close() # don't forget to close it!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With context managers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(\"hi.txt\", \"w\") as f:\n",
    "    f.write(\"hello\")\n",
    "# f is closed for us!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Another advantage.  Even if the code inside the `with` fails (for example, maybe there's not enough drive space to write \"hello\" to the file), the context manager will close the file in the last example.  Not so with the prior example."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Conclusion\n",
    "\n",
    "Special functions are crucial to making new types that are nice to use.  For example, pandas Series and DataFrames make heavy use of special methods.  For example, that's why we can filter rows in a DataFrame using `df[bool_series]` -- the `__getitem__` is smart enough to do something useful with `lookup`."
   ]
  }
 ],
 "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.7.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
