{
"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
}