Explain Python classes and objects to my nephew (+advanced use)

It is common secret that Python programming language has a solid claim to being the fastest-growing major programming language witnessing an extraordinary growth in the last five years, as seen by Stack Overflow traffic. Based on data describing the Stack Overflow question views which go to late 2011, the growth of Python relative to five other major programming languages is plotted.

What’s unique about Python?

Python is an object-oriented programming language. Object-oriented programming (OOP) focuses on creating reusable patterns of code. When working on complex programs in particular, object-oriented programming lets you reuse code and write code that is more readable, which in turn makes it more maintainable. Clever use of classes will ultimate benefit a Data Scientist at the stage of productionize its model.

Classes and Objects

Two of the most important concepts in object-oriented programming are:

  • Class — A blueprint created by a programmer for an object. This defines a set of attributes that will characterise any object that is instantiated from this class.
  • Object — An instance of a class. This is the realised version of the class, where the class is manifested in the program.

These are used to create patterns (in the case of classes) and then make use of the patterns (in the case of objects). Simply put Objects are an encapsulation of variables and functions into a single entity. Objects get their variables and functions from classes. Class is actually the pattern /blueprint for a new object that we can define later.

In the below example, the variable “myobjectx” holds an object of the class “MyClass” that contains the variables and the functions defined within the class called “MyClass”. We manage to access the variable inside of the newly created object “myobjectx” by running the command myobjectx.variable1 and myobjectx.variable2.

class MyClass:
    variable1 = "blah"
    variable2 = "hello"
    
    def function(self):
        print("This is a message inside the class.")

myobjectx = MyClass()
print(myobjectx)
print(myobjectx.variable1)
print(myobjectx.variable2)

#Ouput:
<__main__.ObjectCreator object at 0x106146c18>
blah
hello

At the same time we can access the functions as they are under the class MyClassand are called methods. Methods are a special kind of function that are defined within a class.

class MyClass:
    variable = "blah"

    def function1(self):
        print("This is a message inside the class.")
    
    def function2(self):
        a = 5
        print(a)

myobjectx = MyClass()
myobjectx.function1()
myobjectx.function2()

#Output
This is a message inside the class.
5

The argument to these functions is the word self, which is a reference to objects that are made based on this class. To reference instances (or objects) of the class, self will always be the first parameter, but it need not be the only one.

class MyClass:
    variable = "blah"

    def function(self):
        print("This is a message inside the class.")

myobjectx = MyClass()
myobjecty = MyClass()

myobjecty.variable = "yackity"

print(myobjectx.variable)
print(myobjecty.variable)

# Ouput
blah
yackity

One of the advantages is that we can create different objects that are of the same class(have the same variables and functions defined). However, each object contains independent copies of the variables defined in the class.

Classes are objects too

As soon as you use the keyword CLASS, Python executes it and creates an OBJECT. In the below example when running the class command creates in memory an object with the name “ObjectCreator”.

It is still an object since:

  • you can pass it as a function parameter
  • you can add attributes to it
  • you can assign it to a variable
class ObjectCreator(object):
    pass

def echo(o):
    print(o)

# you can pass it as a function parameter
echo(ObjectCreator) 

print(hasattr(ObjectCreator, 'new_attribute'))

#  you can add attributes to a class
ObjectCreator.new_attribute = 'foo' 
print(hasattr(ObjectCreator, 'new_attribute'))
print(ObjectCreator.new_attribute)

# you can assign a class to a variable
ObjectCreatorMirror = ObjectCreator 
print(ObjectCreatorMirror.new_attribute)

# Output
<class '__main__.ObjectCreator'>
False
True
foo
foo

The Constructor Method

class Shark:
    
    def __init__(self):
        print("This is the constructor method.")
    
    def swim(self):
        print("The shark is swimming.")

    def be_awesome(self):
        print("The shark is being awesome.")


def main():
    sammy = Shark()
    sammy.swim()
    sammy.be_awesome()

if __name__ == "__main__":
    main()

# Output
This is the constructor method.
The shark is swimming.
The shark is being awesome.

By adding the above __init__ method to the Shark class in the program above, the program would print automatically:
"This is the constructor method."

This is because the constructor method is automatically initialised (you will never have to call the __init__() method). You should use this method to carry out any initialising you would like to do with your class objects. For example:

class Shark:
    def __init__(self, name):
        self.name = name

    def swim(self):
        print(self.name + " is swimming.")

    def be_awesome(self):
        print(self.name + " is being awesome.")

def main():
    sammy = Shark("Sammy")
    sammy.be_awesome()
    stevie = Shark("Stevie")
    stevie.swim()

if __name__ == "__main__":
  main()

# Output
Sammy is being awesome.
Stevie is swimming.

Difference between Class and Instances Attributes

While instance attributes are specific to each object, class attributes are the same for all instances.

class Dog:

    # Class Attribute
    species = 'mammal'

    # Initializer / Instance Attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    # instance method
    def speak(self, sound):
        return "{} says {}".format(self.name, sound)


# Instantiate the Dog object
philo = Dog("Philo", 5)
mikey = Dog("Mikey", 6)

# Determine the oldest dog
def get_biggest_number(*args):
    return max(args)

print("{} is {} years old of {} species while {} is {} years old of {} species.".format(
    philo.name, philo.age, philo.species, mikey.name, mikey.age, mikey.species))

print("The oldest dog is {} years old.".format(
    get_biggest_number(philo.age, mikey.age)))

print(mikey.speak("Gruff Gruff"))

# Output
Philo is 5 years old of mammal species while Mikey is 6 years old of mammal species.
The oldest dog is 6 years old.
Mikey says Gruff Gruff

Attributes can be modified

You can change the value of attributes based on some behavior:

class Email:
    def __init__(self):
        self.is_sent = False
    def send_email(self):
         self.is_sent = True

my_email = Email()
print(my_email.is_sent)
my_email.send_email()
print(my_email.is_sent)

# Output
False
True

Python Object Inheritance

Inheritance is the process by which one class takes on the attributes and methods of another. Newly formed classes are called child classes, and the classes that child classes are derived from are called parent classes.

It’s important to note that child classes override or extend the functionality of parent classes. In other words, child classes inherit all of the parent’s attributes and behaviours but can also specify different behaviour to follow.

# Parent class
class Dog:

    # Class attribute
    species = 'mammal'

    # Initializer / Instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # instance method
    def description(self):
        return "{} is {} years old".format(self.name, self.age)

    # instance method
    def speak(self, sound):
        return "{} says {}".format(self.name, sound)


# Child class (inherits from Dog() class)
class RussellTerrier(Dog):
    def run(self, speed):
        return "{} runs {}".format(self.name, speed)


# Child class (inherits from Dog() class)
class Bulldog(Dog):
    def run(self, speed):
        return "{} runs {}".format(self.name, speed)


# Child classes inherit attributes and
# behaviors from the parent class
jim = Bulldog("Jim", 12)
print(jim.description())

# Child classes have specific attributes
# and behaviors as well
print(jim.run("slowly"))

# Is jim an instance of Dog()?
print(isinstance(jim, Dog))

# Is julie an instance of Dog()?
julie = Dog("Julie", 100)
print(isinstance(julie, Dog))

# Is johnny walker an instance of Bulldog()
johnnywalker = RussellTerrier("Johnny Walker", 4)
print(isinstance(johnnywalker, Bulldog))

# Is julie and instance of jim?
print(isinstance(julie, jim))

# Ouptut
Jim is 12 years old
Jim runs slowly
True
True
False
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-c976ae4d1a2c> in <module>
     52 
     53 # Is julie and instance of jim?
---> 54 print(isinstance(julie, jim))

TypeError: isinstance() arg 2 must be a type or tuple of types

It does make sense as jim and julie are instances of the Dog() class, while johnnywalker is not an instance of the Bulldog() class. Then as a sanity check, we tested if julie is an instance of jim, which is impossible since jim is an instance of a class rather than a class itself—hence the reason for the TypeError.

Overriding the Functionality of a Parent Class

As a class can override its class attribute in the same way a child class can also override attributes and behaviours from the parent class. For example:

class Dog:
    species = 'mammal'

class SomeBreed(Dog):
    pass

class SomeOtherBreed(Dog):
    species = 'reptile'

frank = SomeBreed()
print(frank.species)
beans = SomeOtherBreed()
print(beans.species)

# Output
mammal
reptile

The SomeBreed() class inherits the species from the parent class, while the SomeOtherBreed() class overrides the species, setting it to reptile.

Advanced: Why/How to use ABCMeta and @abstractmethod

To understand how this works and why we should use AGCMeta, let’s take a look at an example. Let’s say we have a Base class “LinearModel” with two methods (prepare_data & fit) that must be implemented by all derived classes.

class LinearModel():
    
    def prepare_data(self):
        raise NotImplementedError()
    
    def fit(self):
        raise NotImplementedError()
        
class LinearRegression(LinearModel):
    pass

c = LinearRegression()
c.fit()

# Output
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-36-8b647c03f898> in <module>
      1 c = LinearRegression()
----> 2 c.fit()

<ipython-input-33-e12db09990e4> in fit(self)
      6 
      7     def fit(self):
----> 8         raise NotImplementedError()

NotImplementedError:

When we instantiate an object c and call any of it’s two methods, we’ll get an error (as expected) with the fit() method.

However, this still allows us to instantiate an object of the LinearModel()class without getting an error. In fact we don’t get an error until we look for the fit().

This is avoided by using the Abstract Base Class (ABC) module. Let’s see how this works with the same example:

from abc import ABCMeta, abstractmethod
class LinearModel(metaclass = ABCMeta):
    
    @abstractmethod
    def prepare_data(self):
        raise NotImplementedError()
    
    @abstractmethod
    def fit(self):
        raise NotImplementedError()
        
 class LinearRegression(LinearModel):
    pass

c = LinearRegression()

# Output
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-39-dfea013b4884> in <module>
----> 1 c = LinearRegression()

TypeError: Can't instantiate abstract class LinearRegression with abstract methods fit, prepare_data

This time when we try to instantiate an object from the incomplete class, we immediately get a TypeError! Even if the fit function has been defined properly in the parent class.

from abc import ABCMeta, abstractmethod
class LinearModel(metaclass = ABCMeta):
    
    @abstractmethod
    def fit(self):
        print('Hello')

class LinearRegression(LinearModel):
    pass

c = LinearRegression()

# Output
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-40-6e09917448d0> in <module>
      9     pass
     10 
---> 11 c = LinearRegression()
     12 
     13 # Output

TypeError: Can't instantiate abstract class LinearRegression with abstract methods fit

This force us to complete the class LinearRegression to avoid any Errors:

from abc import ABCMeta, abstractmethod
class LinearModel(metaclass = ABCMeta):
    
    @abstractmethod
    def fit(self):
        print('Hello')

class LinearRegression(LinearModel):
    
    def fit(self):
        print('Bye')

c = LinearRegression()
c.fit()

# Output
Bye

This time when you instantiate an object it works!

Example when this can be useful

If you make a GTA-like game where you can drive different vehicles, you create an abstract class “Vehicle” with an abstract method “drive”. You then have different types of vehicles, each with its own class: “Car”, “Bike”, “Train”, … . Each of these classes will have a different implementation of “drive”. The train will be bound to tracks, the bike to lower speeds, the car to other rules, …

This is much more efficient than having your drive method implemented in “Vehicle”. You don’t need to add all the “if class == Car/Bike/…” bullshit, which is extremely ugly. Also no vehicle will exist as just “Vehicle”, they will always belong to a child class, therefore the “Vehicle” class remains abstract, empty.

Stay tuned for my next article on Python metaclasses.

Conclusion

Object-oriented programming is an important concept to understand because it makes code recycling more straightforward, as objects created for one program can be used in another. Object-oriented programs also make for better program design since complex programs are difficult to write and require careful planning, and this in turn makes it less work to maintain the program over time.

Thanks for reading and I am looking forward to hear your questions 🙂
Stay tuned and Happy Coding.

P.S If you want to learn more of the world of machine learning/coding you can also follow me on Instagram, email me directly or find me on linkedin. I’d love to hear from you. Resources:
https://docs.python.org/3/library/abc.html
https://www.python.org/dev/peps/pep-3115/
https://riptutorial.com/python/example/23083/why-how-to-use-abcmeta-and–abstractmethod

Source: towardsdatascience