Notebook-13: Inheritance and Classes

A (Semi-Brief) Discourse on Families & Inheritance

GeoPandas objects are deliberately designed to resemble Pandas objects. There are two good reasons for this:

  1. Since Pandas is well-known, this makes it easier to learn how to use GeoPandas.
  2. GeoPandas inherits functionality from Pandas.

The concept of inheritance is something we've held off from mentioning until now, but it's definitely worth understanding if you are serious about learning how to code. In effect, geopandas 'imports' pandas and then extends it so that the more basic class (pandas in this case) learns how to work with geodata... pandas doesn't know how to read shapefiles or make maps, but geopandas does. Same for GeoJSON.

The 'Tree of Life'

Here's a simple way to think about inheritance: think of the 'evolutionary trees' you might have seen charting the evolution of organisms over time. At the bottom of the tree is the single-celled animal, and at the other end are humans, whales, wildebeest, etc. We all inherit some basic functionality from that original, simple cell. In between us and that primitive, however, are a whole series of branches: different bits of the tree evolved in different directions and developed different 'functionality'. Some of us have bones. Some have cartilege. Some are vegetarian, and some are carnivorous. And so on. When you get to the primates we all share certain common 'features' (binocular vision, grasping hands, etc.), but we are still more similar to gorillas than we are to macaques. So gorillas and humans extend the primitive 'primate functionality' with some bonus features (bigger brains, greater strength, etc.) that are useful, while macaques extend it with a slightly different set of features (tails, etc.).

The 'Tree of Classes'

Inheritance in code works in a similar way: all Python classes (lists, pandas, plots, etc.) inherit their most basic functionality from a single primitive 'object' class that itself does very little except to provide a template for what an object should look like. As you move along the inheritance tree you will find more and more complex objects with increasingly advanced features: GeoPandas inherits from Pandas, Bokeh and Seaborn inherit from matplotlib, etc.

I can't find an image of Python base class inheritance, but I've found an equally useful example of how anything can be modelled using this 'family tree' approach... consider the following:

If we were trying to implement a vehicle registration scheme in Python, we would want to start with the most basic category of all: vehicle. The vehicle class itself might not do much, but it gives us a template for all vehicles (e.g. it must be registered, it must have a unique license number, etc.). We then extend the functionality of this 'base class' with three intermediate classes: two-wheeled vehicles, cars, and trucks. These, in turn, lead to eight actual vehicle types. These might have additional functionality: a bus might need have a passenger capacity associated with it, while a convertible might need to be hard- or soft-top. All of this could be expressed in Python as:

class vehicle(object): # Inherit from base class
    def __init__(self):
        ... do something ...

class car(vehicle): # Inherit from vehicle
    def __init__(self):
        ... do other stuff ...

class sedan(car): # Inherit from car
    def __init__(self):
        ... do more stuff ...

This way, when we create a new sedan, it automatically 'knows' about vehicles and cars, and can make use of functions like set_unique_id(<identification>) even if that function is only specified in the base vehicle class! The thing to remember is that programmers are lazy: if they can avoid reinventing the wheel, they will. Object-Oriented Programming using inheritance is a good example of constructive laziness: it saves us having to constantly copy and paste code (for registering a new vehicle or reading in a CSV file) from one class to the next since we can just import it and extend it!

Advantages of Inheritance #1

This also means that we are less likely to make mistakes: if we want to update our vehicle registration scheme then we don't need to update lots of functions all over the place, we just update the base class and all inheriting classes automatically gain the update because they are making use of the base class' function.

So if pandas is updated with a new 'load a zip file' feature then geopandas automatically benefits from it! The only thing that doesn't gain that benefit immediately is our ability to make use of specifically geographical data because pandas doesn't know about that type of data, only 'normal' tabular data.

Advantages of Inheritance #2

Inheritance also means that you can always use an instance of a 'more evolved' class in place of one of its ancestors: simplifying things a bit, a sedan can automatically do anything that a car can do and, by extension, anything that a vehicle can do.

Similarly, since geopandas inherits from pandas if you need to use a geopandas object as if it's a pandas object then that will work! So everything you learned last term for pandas can still be used in geopandas. Kind of cool, right?

Designing for Inheritance

Finally, looking back at our example above: what about unicycles? Or tracked vehicles like a tank? This is where design comes into the picture: when we're planning out a family tree for our work we need to be careful about what goes where. And there isn't always a single right answer: perhaps we should distinguish between pedal-powered and motor-powered (in which case unicycles, bicycles and tricycles all belong in the same family)? Or perhaps we need to distinguish between wheeled and tracked (in which case we're missing a pair of classes [wheeled, tracked] between 'vehicle' and 'two-wheel, car, truck')? These choices are tremendously important but often very hard to get right.

OK, that's enough programming theory, let's see this in action...

Classes

Here is a simple demonstration of how classes work and why they're useful in programming. Building on last week's 'volume of a sphere' question here's how we'd create and work with a 'shape' class in Python:


In [1]:
from math import pi

class shape(object): # Inherit from base class 
    def __init__(self): 
        return 
    
    def volume(self):
        raise Exception("Unimplmented method error.")
    
    def diameter(self):
        raise Exception("Unimplmented method error.")
        
    def type(self):
        return(self.shape_type)
    
class cube(shape): # Inherit from shape 
    def __init__(self, e):
        self.shape_type = 'Cube'
        self.edge = e
        return
    
    def volume(self):
        return self.edge**3
    
    def diameter(self):
        return (self.edge**2 + self.edge**2)**(1/2)

class sphere(shape): # Inherit from shape 
    def __init__(self, r):
        self.shape_type = 'Sphere'
        self.radius = r
        return
    
    def volume(self):
        return (4/3) * pi * self.radius**3
    
    def diameter(self):
        return self.radius*2

class pyramid(shape): # Inherit from shape
    
    has_mummies = True # This is for *all* regular pyramids
    
    def __init__(self, e):
        self.shape_type = 'Regular Pyramid'
        self.edge   = e
        return  

class t_pyramid(pyramid): # Inherit from pyramid (this is a triangular pyramid)
    
    has_mummies = False # This is for all triangular pyramids
    
    def __init__(self, e):
        self.shape_type = 'Triangular Pyramid'
        self.edge   = e
        return 
    
    def area(self):
        return (3**(1/2)/4) * self.edge**2
    
    def height(self):
        # https://www.youtube.com/watch?v=ivF3ndmkMsE
        return (6**(1/2) * self.edge/3)
    
    def volume(self):
        #  V = 1/3 * A * h
        return (1/3) * self.area() * self.height()

s = sphere(10)
print(s.type())
print("\tVolume is: {0:5.2f}".format(s.volume()))
print("\tDiameter is: {0:5.2f}".format(s.diameter()))
print("")

c = cube(10)
print(c.type())
print("\tVolume is: {0:5.2f}".format(c.volume()))
print("\tDiameter is: {0:5.2f}".format(c.diameter()))
print("")

p = t_pyramid(10)
print(p.type())
print("\tVolume is: {0:5.2f}".format(p.volume()))
if p.has_mummies is True:
    print("\tMummies? Aaaaaaaaagh!")
else:
    print("\tPhew, no mummies!")

# The error here is deliberate -- note that diameter
# is not implemented in either triangular pyramid, pyramid, 
# or shape, only for spheres.
print("\tDiameter is: {0:5.2f}".format(p.diameter()))
print("")


Sphere
	Volume is: 4188.79
	Diameter is: 20.00

Cube
	Volume is: 1000.00
	Diameter is: 14.14

Triangular Pyramid
	Volume is: 117.85
	Phew, no mummies!
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-1-2566ecf3262f> in <module>
     90 # is not implemented in either triangular pyramid, pyramid,
     91 # or shape, only for spheres.
---> 92 print("\tDiameter is: {0:5.2f}".format(p.diameter()))
     93 print("")

<ipython-input-1-2566ecf3262f> in diameter(self)
      9 
     10     def diameter(self):
---> 11         raise Exception("Unimplmented method error.")
     12 
     13     def type(self):

Exception: Unimplmented method error.

Have a really good think about how this kind of behaviour is useful!

Test Your Understanding

Based on the above examples of classes and methods, I have two challenges for you to implement in the code below:

  1. Try fully-implementing the regular pyramid class with a square base that we skipped over above!
  2. Try adding an area method that returns the surface area of each shape and then add that information to the output below.

In [ ]:
from math import pi

class shape(object): # Inherit from base class 
    def __init__(self): 
        return 
    
    def volume(self):
        raise Exception("Unimplmented method error.")
    
    def diameter(self):
        raise Exception("Unimplmented method error.")
        
    def type(self):
        return(self.shape_type)
    
class cube(shape): # Inherit from shape 
    def __init__(self, e):
        self.shape_type = 'Cube'
        self.edge = e
        return
    
    def volume(self):
        return self.edge**3
    
    def diameter(self):
        return (self.edge**2 + self.edge**2)**(1/2)

class sphere(shape): # Inherit from shape 
    def __init__(self, r):
        self.shape_type = 'Sphere'
        self.radius = r
        return
    
    def volume(self):
        return (4/3) * pi * self.radius**3
    
    def diameter(self):
        return self.radius*2

class pyramid(shape): # Inherit from shape
    
    has_mummies = True # This is for *all* regular pyramids
    
    def __init__(self, e):
        self.shape_type = 'Regular Pyramid'
        self.edge   = e
        return  

class t_pyramid(pyramid): # Inherit from pyramid (this is a triangular pyramid)
    
    has_mummies = False # This is for all triangular pyramids
    
    def __init__(self, e):
        self.shape_type = 'Triangular Pyramid'
        self.edge   = e
        return 
    
    def area(self):
        return (3**(1/2)/4) * self.edge**2
    
    def height(self):
        # https://www.youtube.com/watch?v=ivF3ndmkMsE
        return (6**(1/2) * self.edge/3)
    
    def volume(self):
        #  V = 1/3 * A * h
        return (1/3) * self.area() * self.height()

# How would you test these changes?

Credits!

Contributors:

The following individuals have contributed to these teaching materials:

License

The content and structure of this teaching project itself is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license, and the contributing source code is licensed under The MIT License.

Acknowledgements:

Supported by the Royal Geographical Society (with the Institute of British Geographers) with a Ray Y Gildea Jr Award.

Potential Dependencies:

This notebook may depend on the following libraries: None