{ "cells": [ { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "# Advanced Object Oriented Programming\n", "\n", "\n", "In the regular section on Object Oriented Programming (OOP) we covered:\n", "\n", "* Using the *class* keyword to define object classes\n", "* Creating class attributes\n", "* Creating class methods\n", "* Inheritance - where derived classes can inherit attributes and methods from a base class\n", "* Polymorphism - where different object classes that share the same method can be called from the same place\n", "* Special Methods for classes like `__init__`, `__str__`, `__len__` and `__del__`\n", "\n", "In this section we'll dive deeper into\n", "* Multiple Inheritance\n", "* The `self` keyword\n", "* Method Resolution Order (MRO)\n", "* Python's built-in `super()` function" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Inheritance Revisited\n", "\n", "Recall that with Inheritance, one or more derived classes can inherit attributes and methods from a base class. This reduces duplication, and means that any changes made to the base class will automatically translate to derived classes. As a review:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Fido says Woof!\n", "Isis says Meow!\n" ] } ], "source": [ "class Animal:\n", " def __init__(self, name): # Constructor of the class\n", " self.name = name\n", "\n", " def speak(self): # Abstract method, defined by convention only\n", " raise NotImplementedError(\"Subclass must implement abstract method\")\n", "\n", "\n", "class Dog(Animal):\n", " def speak(self):\n", " return self.name+' says Woof!'\n", " \n", "class Cat(Animal):\n", " def speak(self):\n", " return self.name+' says Meow!'\n", " \n", "fido = Dog('Fido')\n", "isis = Cat('Isis')\n", "\n", "print(fido.speak())\n", "print(isis.speak())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, the derived classes did not need their own `__init__` methods because the base class `__init__` gets called automatically. However, if you do define an `__init__` in the derived class, this will override the base:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class Animal:\n", " def __init__(self,name,legs):\n", " self.name = name\n", " self.legs = legs\n", "\n", "class Bear(Animal):\n", " def __init__(self,name,legs=4,hibernate='yes'):\n", " self.name = name\n", " self.legs = legs\n", " self.hibernate = hibernate\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is inefficient - why inherit from Animal if we can't use its constructor? The answer is to call the Animal `__init__` inside our own `__init__`." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Yogi\n", "4\n", "yes\n" ] } ], "source": [ "class Animal:\n", " def __init__(self,name,legs):\n", " self.name = name\n", " self.legs = legs\n", "\n", "class Bear(Animal):\n", " def __init__(self,name,legs=4,hibernate='yes'):\n", " Animal.__init__(self,name,legs)\n", " self.hibernate = hibernate\n", " \n", "yogi = Bear('Yogi')\n", "print(yogi.name)\n", "print(yogi.legs)\n", "print(yogi.hibernate)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Multiple Inheritance\n", "\n", "Sometimes it makes sense for a derived class to inherit qualities from two or more base classes. Python allows for this with multiple inheritance." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class Car:\n", " def __init__(self,wheels=4):\n", " self.wheels = wheels\n", " # We'll say that all cars, no matter their engine, have four wheels by default.\n", "\n", "class Gasoline(Car):\n", " def __init__(self,engine='Gasoline',tank_cap=20):\n", " Car.__init__(self)\n", " self.engine = engine\n", " self.tank_cap = tank_cap # represents fuel tank capacity in gallons\n", " self.tank = 0\n", " \n", " def refuel(self):\n", " self.tank = self.tank_cap\n", " \n", " \n", "class Electric(Car):\n", " def __init__(self,engine='Electric',kWh_cap=60):\n", " Car.__init__(self)\n", " self.engine = engine\n", " self.kWh_cap = kWh_cap # represents battery capacity in kilowatt-hours\n", " self.kWh = 0\n", " \n", " def recharge(self):\n", " self.kWh = self.kWh_cap" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So what happens if we have an object that shares properties of both Gasolines and Electrics? We can create a derived class that inherits from both!" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n", "0\n" ] } ], "source": [ "class Hybrid(Gasoline, Electric):\n", " def __init__(self,engine='Hybrid',tank_cap=11,kWh_cap=5):\n", " Gasoline.__init__(self,engine,tank_cap)\n", " Electric.__init__(self,engine,kWh_cap)\n", " \n", " \n", "prius = Hybrid()\n", "print(prius.tank)\n", "print(prius.kWh)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "5\n" ] } ], "source": [ "prius.recharge()\n", "print(prius.kWh)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Why do we use `self`?\n", "\n", "We've seen the word \"self\" show up in almost every example. What's the deal? The answer is, Python uses `self` to find the right set of attributes and methods to apply to an object. When we say:\n", "\n", " prius.recharge()\n", "\n", "What really happens is that Python first looks up the class belonging to `prius` (Hybrid), and then passes `prius` to the `Hybrid.recharge()` method.\n", "\n", "It's the same as running:\n", "\n", " Hybrid.recharge(prius)\n", " \n", "but shorter and more intuitive!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Method Resolution Order (MRO)\n", "Things get complicated when you have several base classes and levels of inheritance. This is resolved using Method Resolution Order - a formal plan that Python follows when running object methods.\n", "\n", "To illustrate, if classes B and C each derive from A, and class D derives from both B and C, which class is \"first in line\" when a method is called on D?
Consider the following:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class A:\n", " num = 4\n", " \n", "class B(A):\n", " pass\n", "\n", "class C(A):\n", " num = 5\n", " \n", "class D(B,C):\n", " pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Schematically, the relationship looks like this:\n", "\n", "\n", " A\n", " num=4\n", " / \\\n", " / \\\n", " B C\n", " pass num=5\n", " \\ /\n", " \\ /\n", " D\n", " pass\n", "\n", "Here `num` is a class attribute belonging to all four classes. So what happens if we call `D.num`?" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "D.num" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You would think that `D.num` would follow `B` up to `A` and return **4**. Instead, Python obeys the first method in the chain that *defines* num. The order followed is `[D, B, C, A, object]` where *object* is Python's base object class.\n", "\n", "In our example, the first class to define and/or override a previously defined `num` is `C`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## `super()`\n", "\n", "Python's built-in `super()` function provides a shortcut for calling base classes, because it automatically follows Method Resolution Order.\n", "\n", "In its simplest form with single inheritance, `super()` can be used in place of the base class name :" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "class MyBaseClass:\n", " def __init__(self,x,y):\n", " self.x = x\n", " self.y = y\n", " \n", "class MyDerivedClass(MyBaseClass):\n", " def __init__(self,x,y,z):\n", " super().__init__(x,y)\n", " self.z = z\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that we don't pass `self` to `super().__init__()` as `super()` handles this automatically.\n", "\n", "In a more dynamic form, with multiple inheritance like the \"diamond diagram\" shown above, `super()` can be used to properly manage method definitions:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "class A:\n", " def truth(self):\n", " return 'All numbers are even'\n", " \n", "class B(A):\n", " pass\n", "\n", "class C(A):\n", " def truth(self):\n", " return 'Some numbers are even'\n", " \n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'All numbers are even'" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class D(B,C):\n", " def truth(self,num):\n", " if num%2 == 0:\n", " return A.truth(self)\n", " else:\n", " return super().truth()\n", " \n", "d = D()\n", "d.truth(6)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'Some numbers are even'" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "d.truth(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the above example, if we pass an even number to `d.truth()`, we'll believe the `A` version of `.truth()` and run with it. Otherwise, follow the MRO and return the more general case." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For more information on `super()` visit https://docs.python.org/3/library/functions.html#super
and https://rhettinger.wordpress.com/2011/05/26/super-considered-super/" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Great! Now you should have a much deeper understanding of Object Oriented Programming!" ] } ], "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.2" } }, "nbformat": 4, "nbformat_minor": 1 }