Object-Oriented Programming and Python

To quote one of the first sentences from the wikipedia page on Python:

Python supports multiple programming paradigms, including object-oriented, imperative and functional programming or procedural styles.

In the Introductory notes I also referred to modules functions as well as member functions.

These bits of jargon are related; everything in Python is an object.

In the context of programming, objects

“…are data structures that contain data, in the form of fields, often known as attributes; and code, in the form of procedures, often known as methods.” https://en.wikipedia.org/wiki/Object-oriented_programming

Everything in Python is an object (even functions!)

Every entity in a python script has both data associated with it, and functions.

For example, even the most basic seeming data is an object:

print( (1).__class__ )

yields int - ie the class name attribute of the number 1 is int (the parentheses are needed to distinguish the “.” from a decimal sign!).

Note that this is different from basic programming concept of data type!

As proof, the full list of member functions for our number are:

bit_length   denominator  imag         real        
conjugate    from_bytes   numerator    to_bytes  

i.e. functions to e.g. get the real and imaginary parts of the number, as well as return a “bytes” (binary) representation.

As well as these member functions, as with most Python objects there are a host of hidden member functions:

__abs__           __init__          __rlshift__
__add__           __int__           __rmod__
__and__           __invert__        __rmul__
__bool__          __le__            __ror__
__ceil__          __lshift__        __round__
__class__         __lt__            __rpow__
__delattr__       __mod__           __rrshift__
__dir__           __mul__           __rshift__
__divmod__        __ne__            __rsub__
__doc__           __neg__           __rtruediv__
__eq__            __new__           __rxor__
__float__         __or__            __setattr__
__floor__         __pos__           __sizeof__
__floordiv__      __pow__           __str__
__format__        __radd__          __sub__
__ge__            __rand__          __subclasshook__
__getattribute__  __rdivmod__       __truediv__
__getnewargs__    __reduce__        __trunc__
__gt__            __reduce_ex__     __xor__
__hash__          __repr__          
__index__         __rfloordiv__  

Why the funny notation for these member functions?

Unlike e.g. C++ or Java, in Python there is no mechanism to declare member functions as private vs public (or protected) – this jargon is all to do with OOP, specifically one of the core concepts known as inheritance. More on that below!

Instead, in Python the notion of private member functions is replaced by hidden member functions; technically they’re not private and can be overridden in child classes, but it is suggested by the authors of the class that you don’t!

If you were to open an IPython interactive shell and type print.__ and then hit tab you’d see that even the print function is an object. Functions are objects too!

So now that we’ve established that even “simple” numbers and functions are all objects, lets look a bit more at what an object is.

Why Objects?

There are a few main concepts at the heart of OOP, including

  • Encapsulation: hide information about how a class does what it does from calling code; instead present an interface via member functions.
    • Facilitates code refactoring - a change of an objects internals shouldn’t affect code that uses that object
  • Composition: Objects can contain other objects as their member variables
  • Inheritance: Classes (the blueprints for objects) can derive from base classes allowing a heirarchy of functionality and data
  • Polymorphism: When calling code is “agnostic” to whether an object belongs to a parent class or one of it’s descendants.

Ok? Probably not! We have so far answered a question with a bunch of jargon. Let’s make this a little clearer with an example.

Simple OOP example: Geometric shapes classes

As an example, let’s consider that we’re writing a program that works with geometric shapes like circles, squares, triangles etc.

For each type of shape we might want to calculate things like perimeter, area, bounding box etc.

The OOP way to solve this task would be to first of all create a parent class that contains the features common to all shapes. For example

class Shape():
    pass

would define a class called Shape (the pass statement is needed as we need at least one line of code in the class-block).

Next, we could add in some member attributes - data associated with our class - and member functions, (i.e. the class will encapsulate all required functionality).

class Shape():
    name    = "Generic shape"
    centre  = []

    def print_name(self):
        print(self.name)

    def get_area(self):
        return None

Here we have 2 member attributes, and 2 member functions.

You might have spotted the funny (required!) first argument in the definition of the member functions; we called it self, which is a convention, as it is a variable holding the object itself. This is how we have access an object’s other attributes and member functions, inside a member function.

The member function called get_area returns a None, because a generic shape doesn’t exist, and can’t have an area!

Now, as we said, generic shapes don’t exist, we only have concrete shapes! So let’s create a very simple first child class; the square

class Square(Shape):
    name    = "Square"

The Shape in the parentheses of the class definition line, mean that the Square class will inherit from the Shape class.

If we create an object of class Square, it will automatically have inherited the print_name member function:

square1 = Square()
square1.print_name()

prints Square to the console. Similarly, we might want to have a triangle

class Triangle(Shape):
    name= "Triangle"

Now at the moment, except for printing out the correct name, all of our classes will return None when calling get_area. But, for squares and triangles, we should be able to get a meaningful value!

Now we need to add in some child-class specific code. First of all, when creating a square , we need to know a single dimension for the size. To achieve this we add a member attribute, width, and over-write the get_area function:

class Square(Shape):
    name    = "Square"
    width   = 0        # A default value 

    def get_area(self):
        return width * width

Similarly for the triangle

class Triangle(Shape):
    name    = "Triangle"
    width   = 0
    height  = 0

    def get_area(self):
        return 0.5 * width * height 

Almost there! We’ve added some child-class specific functionality now so that each concrete shape class should report the correct area.

But wait, we haven’t created a good way to assign the required data yet.

The full OOP approach would include creating setter and getter functions for each attribute that we want anyone using the class to be able to access.

However, for brevity, lets override the default constructor function (called when an object of a class is created). In Python that function is named __init__.

class Square(Shape):
    name    = "Square"
    width   = 0        # A default value 

    def __init__(self, width):
        self.width = width

    def get_area(self):
        return width * width

class Triangle(Shape):
    name    = "Triangle"
    width   = 0
    height  = 0

    def __init__(self, width, height):
        self.width = width
        self.height = height 

    def get_area(self):
        return 0.5 * width * height 

Now, regardless of what shape we’re dealing with, we can interrogate the area using get_area and receive the correct response! This is closely related to polymorphism as we can call the same function on either child class and get meaningful output.

NOTE: as we specified width and height as positional arguments, Triangles and Squares must now always be created with the width and width and height values as inputs (respectively).

We have not covered composition in the above treatment, as this most commonly emerges when dealing with more complex classes. Nonetheless, this gives a basic introduction to classes and OOP with Python, and hopefully sheds some light on the “why” of member-functions!