{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Object Oriented Python" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Guidelines\n", "\n", "- Do you need custom classes?\n", "- Often easier to work with just functions and data structures\n", "- Favor composition over inheritance\n", "- Avoid inheriting from concrete classes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Object-oriented ideas\n", "\n", "- What is an object?\n", "- Classes and instances\n", "- Inheritance\n", "- Composition\n", "- Abstract base classes\n", "- Design patterns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Class creation basics" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class A:\n", " \"\"\"Class A\"\"\"\n", " def __init__(self, x):\n", " self.x = x\n", " \n", " def __repr__(self):\n", " return '{}({})'.format(self.__class__.__name__, self.x)\n", " \n", " def report(self):\n", " return self.x\n", " \n", "class B(A):\n", " \"\"\"Class B inherits from A.\"\"\"\n", " def __init__(self, x, y):\n", " self.y = y\n", " # super() returns the first parent class with the a method called __init__\n", " super().__init__(x)\n", " \n", " def report(self):\n", " \"\"\"Over-ridden method.\"\"\"\n", " return self.x, self.y\n", " " ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "a = A(1)\n", "b = B(2,3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**This uses the `__repr__` method found only in A**" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(A(1), B(2))" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a, b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**This uses the instance specific method `report`**" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a.report()" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(2, 3)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "b.report()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Classes are factories for instances" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[A(0), A(1), A(2), A(3), A(4)]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create a bunch of instances of A with differnet values for x\n", "\n", "[A(i) for i in range(5)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Class attributes and methods" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'type'" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "A.__class__.__name__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Instance attributes and methods" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'A'" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a.__class__.__name__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Special methods](https://docs.python.org/3/reference/datamodel.html#special-method-names)\n", "\n", "See also [Special Method Names](http://www.diveintopython3.net/special-method-names.html) for a great summary." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Generic special methods" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "You have seen some of these such as `__init__`, `__repr__`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Special methods for containers" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Example" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def fib(max):\n", " \"\"\"A Fibonacci generator.\"\"\"\n", " \n", " a, b = 0, 1\n", " while b < max:\n", " a, b = b, a + b\n", " yield a" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(fib(100))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "We make the class Fib a container for Fibonacci numbers up to a maximum." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class Fib:\n", " \"\"\"A Fibonacci class.\"\"\"\n", " \n", " def __init__(self, max):\n", " self.max = max\n", " self.count = 0\n", " \n", " def __repr__(self):\n", " return 'Fib({})'.format(self.max)\n", " \n", " def __iter__(self):\n", " self.a, self.b = 0, 1\n", " return self\n", " \n", " def __next__(self):\n", " if self.b > self.max:\n", " raise StopIteration()\n", " self.a, self.b = self.b, self.a + self.b\n", " return self.a" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": true }, "outputs": [], "source": [ "f1 = Fib(100)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ "f2 = Fib(20)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(Fib(100), Fib(20))" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f1, f2" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(f1)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[1, 1, 2, 3, 5, 8, 13]" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(f2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Special methods for numbers" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import math\n", "from functools import total_ordering\n", "\n", "@total_ordering\n", "class Point:\n", " def __init__(self, x, y):\n", " self.x = x\n", " self.y = y\n", " \n", " def __repr__(self):\n", " return 'Point({},{})'.format(self.x, self.y)\n", " \n", " def __add__(self, other):\n", " x = self.x + other.x\n", " y = self.y + other.y\n", " return Point(x, y)\n", " \n", " def __radd__(self, otther):\n", " return self.__add__(other)\n", " \n", " def __eq__(self, other):\n", " return (self.x, self.y) == (other.x, other.y)\n", " \n", " def __lt__(self, other):\n", " return (self.x, self.y) < (other.x, other.y)\n", " \n", " def mag(self):\n", " return math.sqrt(self.x**2 + self.y**2)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": true }, "outputs": [], "source": [ "p1 = Point(2,3)\n", "p2 = Point(1,2)\n", "p3 = Point(3,5)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Point(3,5)" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1 + p2" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Point(3,5)" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p2 + p1" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1 == p2" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p3 == p1 + p2" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Point(1,2), Point(2,3), Point(3,5)]" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sorted([p1, p2, p3])" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5.830951894845301" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p3.mag()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Properties\n", "\n", "Properties allow classes to use the convenient attribute access syntax while using a more flexible getter/setter approach under the hood." ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class A:\n", " def __init__(self, cost, discount=False):\n", " self._cost = cost\n", " self.discount = discount\n", " \n", " @property\n", " def cost(self):\n", " if self.discount:\n", " return 0.9*self._cost\n", " else:\n", " return self._cost \n", " \n", " @cost.setter\n", " def cost(self, value):\n", " if value < 0:\n", " self._cost = 0\n", " else:\n", " self._cost = value" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": true }, "outputs": [], "source": [ "a1 = A(100)\n", "a2 = A(100, True)" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "100" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a1.cost" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "90.0" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a2.cost" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": true }, "outputs": [], "source": [ "a1.cost = -10" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a1.cost" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Inheritance\n", "\n", "- Use the super() syntax to call super class attributes and methods\n", "- Avoid redundant super() calls - undefined attributes and methods will be searched in the inheritance tree automatically" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class A:\n", " \"\"\"Base class.\"\"\"\n", " \n", " def __init__(self, x):\n", " self.x = x\n", " \n", " def __repr__(self):\n", " \"\"\"Flexible string representation for any number of initialized variables.\"\"\"\n", " return '{}({})'.format(\n", " self.__class__.__name__, \n", " ', '.join('{}={}'.format(k, v) \n", " for k, v in sorted(self.__dict__.items())))\n", "\n", "class B(A):\n", " \"\"\"B extends A.\"\"\"\n", " \n", " def __init__(self, x, y):\n", " \"\"\"__init__ over-rides __init__ in A.\"\"\"\n", " self.y = y\n", " super().__init__(x)\n", " \n", "class C(B):\n", " \"\"\"C extends B.\"\"\"\n", " \n", " def f(self):\n", " return self.x, self.y" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "collapsed": true }, "outputs": [], "source": [ "a = A(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "B uses its own `__init__` that accepts two argumens." ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "collapsed": true }, "outputs": [], "source": [ "b = B(2,3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "C uses the same `__init__` as B since it does not define its own." ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "collapsed": true }, "outputs": [], "source": [ "c = C(4,5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All instances use the `__repr__` from A." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(A(x=1), B(x=2, y=3), C(x=4, y=5))" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a, b, c" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `f()` method is onvly available in instances of C." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(4, 5)" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "c.f()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Chaining methods" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class Num:\n", " def __init__(self, val):\n", " self.val = val\n", " \n", " def incr(self):\n", " self.val += 1\n", " print(self.val)\n", " return self\n", " \n", " def decr(self):\n", " self.val -= 1\n", " print(self.val)\n", " return self\n", " \n", " def __repr__(self):\n", " return '{}({})'.format(self.__class__.__name__, self.val)" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "11\n", "12\n", "11\n", "10\n", "9\n", "8\n", "9\n" ] }, { "data": { "text/plain": [ "Num(9)" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "n = Num(10)\n", "n = n.incr().incr().decr().decr().decr().decr().incr()\n", "n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Composition" ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import numpy as np\n", "\n", "class Character:\n", " def __init__(self, name, health, strength, weapon, armor):\n", " self.name = name\n", " self.health = health\n", " self.strength = strength\n", " self.weapon = weapon\n", " self.armor = armor\n", " \n", " def attack(self, other):\n", " if self.health >= 0:\n", " damage = max(0, self.weapon.hit - other.armor.block + \n", " np.random.randint(0, self.strength))\n", " other.health -= damage\n", " return damage\n", " \n", " def __repr__(self):\n", " return self.name\n", " \n", "class Weapon:\n", " def __init__(self, damage, enchanted=False):\n", " self.damage = damage\n", " self.enchanted = enchanted\n", " \n", " @property\n", " def hit(self):\n", " if self.enchanted:\n", " return 2 * self.damage\n", " else:\n", " return self.damage\n", " \n", "class Armor:\n", " def __init__(self, protection, enchanted=False):\n", " self.protection = protection\n", " self.enchanted = enchanted\n", " \n", " @property\n", " def block(self):\n", " if self.enchanted:\n", " return 2 * self.protection\n", " else:\n", " return self.protection" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "collapsed": true }, "outputs": [], "source": [ "axe = Weapon(10)\n", "magic_arrow = Weapon(5, enchanted=True)\n", "loin_cloth = Armor(1)\n", "chainmail = Armor(10)\n", "enchanged_chainmail = Armor(10, )\n" ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Groo starts with 50 health points\n", "Legolas starts with 20 health points\n", "Gimli starts with 25 health points\n", "\n", "\n", "Battle starts\n", "\n", "\n", "Gimli is attacking Legolas for 0 damage!\n", "Groo: 50\n", "Legolas: 20\n", "Gimli: 25\n", "----------------------------------------\n", "Groo is attacking Legolas for 7 damage!\n", "Groo: 50\n", "Legolas: 13\n", "Gimli: 25\n", "----------------------------------------\n", "Gimli is attacking Legolas for 3 damage!\n", "Groo: 50\n", "Legolas: 10\n", "Gimli: 25\n", "----------------------------------------\n", "Groo is attacking Gimli for 7 damage!\n", "Groo: 50\n", "Legolas: 10\n", "Gimli: 18\n", "----------------------------------------\n", "Gimli is attacking Legolas for 6 damage!\n", "Groo: 50\n", "Legolas: 4\n", "Gimli: 18\n", "----------------------------------------\n", "Groo is attacking Legolas for 8 damage!\n", "Groo: 50\n", "Legolas: -4\n", "@ Legolas has died!\n", "----------------------------------------\n", "Gimli is attacking Groo for 15 damage!\n", "Groo: 35\n", "Gimli: 18\n", "----------------------------------------\n", "Gimli is attacking Groo for 16 damage!\n", "Groo: 19\n", "Gimli: 18\n", "----------------------------------------\n", "Gimli is attacking Groo for 9 damage!\n", "Groo: 10\n", "Gimli: 18\n", "----------------------------------------\n", "Gimli is attacking Groo for 12 damage!\n", "Groo: -2\n", "@ Groo has died!\n", "@ Gimli is the winnder!\n" ] } ], "source": [ "# Here we use composition\n", "orc = Character('Groo', health=50, strength=10, weapon=axe, armor=loin_cloth)\n", "elf = Character('Legolas', health=20, strength=5, weapon=magic_arrow, armor=enchanged_chainmail)\n", "dwarf = Character('Gimli', health=25, strength=10, weapon=axe, armor=chainmail)\n", "\n", "players = [orc, elf, dwarf]\n", "\n", "for p in players:\n", " print('{} starts with {} health points'.format(p, p.health))\n", "\n", "print('\\n\\nBattle starts\\n\\n')\n", "\n", "while True:\n", " p_idx = list(range(len(players)))\n", " p1_idx = np.random.choice(p_idx)\n", " p_idx.remove(p1_idx)\n", " p2_idx = np.random.choice(p_idx)\n", " \n", " p1 = players[p1_idx]\n", " p2 = players[p2_idx]\n", " \n", " damage = p1.attack(p2)\n", " print('{} is attacking {} for {} damage!'.format(p1, p2, damage))\n", " \n", " for p in players:\n", " print('{}: {}'.format(p, p.health))\n", " if p.health < 0:\n", " print('@ {} has died!'.format(p))\n", " players.remove(p)\n", " if len(players) == 1:\n", " print('@ {} is the winnder!'.format(players.pop()))\n", " break\n", " print('-'*40)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Abstract base classes" ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import abc" ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class A(abc.ABC):\n", " @abc.abstractclassmethod\n", " def f(self):\n", " \"\"\"Abstract method. Must be over-ridden in derived classes.\"\"\"\n", " \n", "class A1(A):\n", " pass\n", "\n", "class A2(A):\n", " def f(self):\n", " print('This is OK!')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We cannot instantiate an abstract class with abstract methods." ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "Can't instantiate abstract class A with abstract methods f", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0ma\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mA\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: Can't instantiate abstract class A with abstract methods f" ] } ], "source": [ "a = A()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since A1 does not provide an implementation of `f()`, it cannot be instantiated either." ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "Can't instantiate abstract class A1 with abstract methods f", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0ma1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mA1\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: Can't instantiate abstract class A1 with abstract methods f" ] } ], "source": [ "a1 = A1()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A2 can be instantiated because it provides a concrete implementation for `f()`." ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "collapsed": true }, "outputs": [], "source": [ "a2 = A2()" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This is OK!\n" ] } ], "source": [ "a2.f()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.1" } }, "nbformat": 4, "nbformat_minor": 2 }