#!/usr/bin/env python # coding: utf-8 # # Advanced Object Oriented Programming # # # In the regular section on Object Oriented Programming (OOP) we covered: # # * Using the *class* keyword to define object classes # * Creating class attributes # * Creating class methods # * Inheritance - where derived classes can inherit attributes and methods from a base class # * Polymorphism - where different object classes that share the same method can be called from the same place # * Special Methods for classes like `__init__`, `__str__`, `__len__` and `__del__` # # In this section we'll dive deeper into # * Multiple Inheritance # * The `self` keyword # * Method Resolution Order (MRO) # * Python's built-in `super()` function # ## Inheritance Revisited # # 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: # In[1]: class Animal: def __init__(self, name): # Constructor of the class self.name = name def speak(self): # Abstract method, defined by convention only raise NotImplementedError("Subclass must implement abstract method") class Dog(Animal): def speak(self): return self.name+' says Woof!' class Cat(Animal): def speak(self): return self.name+' says Meow!' fido = Dog('Fido') isis = Cat('Isis') print(fido.speak()) print(isis.speak()) # 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: # In[2]: class Animal: def __init__(self,name,legs): self.name = name self.legs = legs class Bear(Animal): def __init__(self,name,legs=4,hibernate='yes'): self.name = name self.legs = legs self.hibernate = hibernate # 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__`. # In[3]: class Animal: def __init__(self,name,legs): self.name = name self.legs = legs class Bear(Animal): def __init__(self,name,legs=4,hibernate='yes'): Animal.__init__(self,name,legs) self.hibernate = hibernate yogi = Bear('Yogi') print(yogi.name) print(yogi.legs) print(yogi.hibernate) # ## Multiple Inheritance # # Sometimes it makes sense for a derived class to inherit qualities from two or more base classes. Python allows for this with multiple inheritance. # In[4]: class Car: def __init__(self,wheels=4): self.wheels = wheels # We'll say that all cars, no matter their engine, have four wheels by default. class Gasoline(Car): def __init__(self,engine='Gasoline',tank_cap=20): Car.__init__(self) self.engine = engine self.tank_cap = tank_cap # represents fuel tank capacity in gallons self.tank = 0 def refuel(self): self.tank = self.tank_cap class Electric(Car): def __init__(self,engine='Electric',kWh_cap=60): Car.__init__(self) self.engine = engine self.kWh_cap = kWh_cap # represents battery capacity in kilowatt-hours self.kWh = 0 def recharge(self): self.kWh = self.kWh_cap # 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! # In[5]: class Hybrid(Gasoline, Electric): def __init__(self,engine='Hybrid',tank_cap=11,kWh_cap=5): Gasoline.__init__(self,engine,tank_cap) Electric.__init__(self,engine,kWh_cap) prius = Hybrid() print(prius.tank) print(prius.kWh) # In[6]: prius.recharge() print(prius.kWh) # ## Why do we use `self`? # # 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: # # prius.recharge() # # What really happens is that Python first looks up the class belonging to `prius` (Hybrid), and then passes `prius` to the `Hybrid.recharge()` method. # # It's the same as running: # # Hybrid.recharge(prius) # # but shorter and more intuitive! # ## Method Resolution Order (MRO) # 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. # # 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: # In[7]: class A: num = 4 class B(A): pass class C(A): num = 5 class D(B,C): pass # Schematically, the relationship looks like this: # # # A # num=4 # / \ # / \ # B C # pass num=5 # \ / # \ / # D # pass # # Here `num` is a class attribute belonging to all four classes. So what happens if we call `D.num`? # In[8]: D.num # 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. # # In our example, the first class to define and/or override a previously defined `num` is `C`. # ## `super()` # # Python's built-in `super()` function provides a shortcut for calling base classes, because it automatically follows Method Resolution Order. # # In its simplest form with single inheritance, `super()` can be used in place of the base class name : # In[9]: class MyBaseClass: def __init__(self,x,y): self.x = x self.y = y class MyDerivedClass(MyBaseClass): def __init__(self,x,y,z): super().__init__(x,y) self.z = z # Note that we don't pass `self` to `super().__init__()` as `super()` handles this automatically. # # In a more dynamic form, with multiple inheritance like the "diamond diagram" shown above, `super()` can be used to properly manage method definitions: # In[10]: class A: def truth(self): return 'All numbers are even' class B(A): pass class C(A): def truth(self): return 'Some numbers are even' # In[11]: class D(B,C): def truth(self,num): if num%2 == 0: return A.truth(self) else: return super().truth() d = D() d.truth(6) # In[12]: d.truth(5) # 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. # 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/ # Great! Now you should have a much deeper understanding of Object Oriented Programming!