A practical guide to Object Oriented Programming in Python

Object-oriented programming is one of the most efficient ways of writing programs. It is an approach to programming where programs are organized into classes and objects.

In this article and you will learn how to:

  • Design classes python.
  • Instantiate objects.
  • Extend and override classes through inheritance.
  • Combining objects through composition.
  • Apply the principles of object-oriented programming in software development.

Object Oriented Programming

Objects are the building blocks of object-oriented programming (OOP). Following the principles of object-oriented programming, you can write programs that model real-life objects such as cars, buildings, trees, animals, people and even places.

Let’s say that you want to build a house. It’s inevitable that you would need a building plan.

In as much as the plan is not the real building, there would be no real building without it. Thus, the building plan is the template upon which the physical building is built.

object oriented programming concept
A building plan is a template for the real building

In essence, whether you want to build a house, car, furniture or bridge, you need a plan or blueprint. In object-oriented programming, blueprints are referred to as classes and the thing you want to build is referred to as an object.

Class as a template for objects

While classes provide the template used in creating objects, the object itself refers to only a specific object. With a building plan, you can construct as many buildings as you wish from it. In the same manner, with a class, you can create as many objects as you wish from it.

Now, let’s look at it this way.

Imagine that you want to build an estate comprising several houses.

Building an entire estate at once may be overwhelming.

An effective approach would be to break down the project into smaller projects. Instead of building all the houses at a time, you may decide to build one house at a time, until the entire estate is completed.

The same concept is applicable in programming. Instead of tackling a complex project head-on, you can break it down into several smaller units or objects.

Hence, it’s right to say that object-oriented programming is programming with the objects in mind.

The principles of object-oriented programming are the same regardless of the programming language you are using, but in this tutorial, we will be looking at how to implement it in the python programming language.

Creating classes

Classes are the templates upon which objects are created. In python, a class is created using the keyword class followed by the name of the class and a colon. An indented block of code under this definition serves as the body of the class.

class Building:
    #do nothing
    pass

The example above is a class with the name Building.

You have the freedom to name your classes. Whatever you wish to use should be valid identifiers names.

It is a good practice to use names that are simple and descriptive. This makes it possible for other people to easily figure out what a class does from the name.

By convention, class names or identifiers are capitalized.

Components of a class

Basically, a class defines the attributes and behaviours of the objects to be built from it.

Attributes are the features of an object that can be used to describe it, while behaviours are things that it can do.

For instance, you can describe a car with attributes such as manufacturer, type, model and colour. In terms of behaviours, a car can start, move, stop, horn and so on.

When writing classes, attributes are expressed as instance variables, while behaviours are expressed as methods.

The example below is a class with the name Car with an attribute self.colour and a method move().

class Car:
    def __init__(self, colour):
        #attribute or instance variable
        self.colour = colour

    def move(self):
        #print moving
        print("Moving")

Constructors

When defining classes, constructors are provided to serve as the first block of code(s) to be executed whenever an object is being created from them.

In Python, a special method with the name __init__ () serves as the constructor. This is the place where you define and assign values to instance variables.

It’s not compulsory to include a constructor in your class, but it is good practice.

class Car:
    def __init__(self):
        #assign value to instance variable self.colour
        self.colour = 'Black'

Instance variables

Instance variables are prefixed with the keyword self. An instance variable is defined by specifying the keyword self, followed by a dot and the name of the variable.

Instance variables are defined inside the constructor or the __init__() method.

This is a way of tying together a variable and an instance.

Every instance of a class has its own copy of instance variables.

The example below is a class with the name Car and three instance variables – self.name, self. manufacturer and self.colour.

class Car:
    def __init__(self, name, manufacturer, colour):
        #define and assign values to instance variables
        self.name = name
        self.manufacturer = manufacturer
        self.colour = colour

Class variables

Variables defined at the top level of a class are known as class variables.  All the instances of the class have access to this variable as well as the values associated with them.

In the example below, a class variable named quantity is defined in the class. Every instance of this class has access to this variable and can modify or alter it.

class Car:
    #class variable quantity
    quantity = 10
    def __init__(self, name, manufacturer, colour):
        #assign values to instance variables
        self.name = name
        self.manufacturer = manufacturer
        self.colour = colour

Now, let’s create two instances of the class car and see how to access and modify the class variable.

class Car:
    #class variable quantity
    quantity = 10
    def __init__(self, name, manufacturer, colour):
        #assign values to instance variables
        self.name = name
        self.manufacturer = manufacturer
        self.colour = colour

#instantiating the object
car1 = Car('Camry', 'Toyota', 'Black')
car2 = Car('Accord', 'Honda', 'White')

print(car1.quantity)
print(car2.quantity)

#modify the class variable
car1.quantity = 7
print(car1.quantity)

car2.quantity = 2
print(car2.quantity)

output

10
10
7
2

Mind you, there is a difference between instance variables and class variables.

Instance variables are defined inside the __init__() method, while class variables are defined at the top level of a class.

In the above example, the variable quantity is a class variable, while variables like self.name and self. manufacturer are instance variables.

Methods

A method is a function defined inside of a class. However, unlike normal functions, methods contain a compulsory parameter named self. This parameter comes before any other parameters in a method’s definition.

The self parameter represents the object from which the method is being accessed.

Whenever there’s a method call, python automatically passes the object as an argument to the self parameter.

class Car:
    def __init(self, name, manufacturer, colour):
        #define and assign values to instance variables
        self.name = name
        self.manufacturer = manufacturer
        self.colour = colour
    def start(self):
        #method to start the car
        print('starting')

    def move(self):
        #method to move the car
        print('moving')

    def car_info(self):
        #method to get information about the car
        info = f'Car information: {self.colour} {self.name} by {self.manufacturer}'
        print(info)

Instantiating objects

Objects are created from classes and the process of creating objects is known as instantiation.

An object of a class has access to all the attributes and methods defined in the class. The syntax for instantiating an object from a class is shown below.

obj = Car('Sunny', 'Nissan', 'black')

You can create multiple instances of the same class, but with different names.

 
obj1 = Car('Sunny', 'Nissan', 'black') 
obj2 = Car('Camry', 'Toyota', 'blue') 
obj3 = Car('Accord', 'Honda', 'white') 

Accessing attributes and methods of a class

You can access the attributes and methods defined in class through the object.

When accessing a method from an object, the object is passed as an argument using the keyword self.

To access attributes and methods of this object, simply provide the name of the object, which in this case is obj, a dot (.) and then the name of the attribute or method.

In the example below, obj is an object of the class Car.

class Car: 
    def __init(self, name, manufacturer, colour):
        #assign instance variables with values 
        self.name = name 
        self.manufacturer = manufacturer 
        self.colour = colour
    def start(self):
        #method to start the car 
        print('starting') 
    def move(self):
        #method to move the car 
        print('moving') 
    def car_info(self):
        #method to get information about the car 
        info = f'Car information: {self.colour} {self.name} by {self.manufacturer}' 
        print(info)
obj = Car('Sunny', 'Nissan', 'Black')
obj.car_info()
obj.start()
obj.move()

output

Black Sunny by Nissan

Modifying the attributes in a class

You can alter or modify the attributes defined in a class from the instance. This is by changing the values of the instance variables or through an interface or method defined in the class.

The example below shows how to alter the value of an instance variable.

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

    def rename(self, new_name):
        self.name = new_name

#Accessing the value of an instance variable
obj = Car('Honda')
print(obj.name)

#modifying the instance variable through the instance
obj.name = 'BMW'
print(obj.name)

#modifying the instance variable through an interface
obj.rename('Toyota')
print(obj.name)

   

output


Honda
BMW
Toyota

It is not a good practice to alter an instance variable directly through the instance. The best way to do this is through the use of an interface or a specific method provided for such an operation. In this case, an interface or method named rename() is used to modify the instance variable self.name.

This prevents the user from performing alterations that might introduce errors in your program.

Encapsulation

The concept of encapsulation in object-oriented programming is simply a way of combining attributes and behaviours as a single unit. The essence is to conceal certain attributes and methods from direct access by other objects.

Objects are not supposed to expose all of their attributes and behaviours. Also, objects should not reveal the internal details of the implementations of their methods.

Hence, interfaces are provided as gateways to objects’ attributes and methods. When writing your class, it’s best that you provide methods known as interfaces from which attributes of the class can be modified or accessed.

Let’s take a look at this example:

class Shape:
    def __init__(self, length, width):
        #define and assign values to instance variables
        self.length = length
        self.width =  width

    def change_length(self, new_length):
        #change the value of self.length > 0:
            self.length = new_length
        else:
            print('enter value greater than 0')
    def change_width(self, new_width):
        #change the value of self.width
        if new_width > 0:
            self.width = new_width
        else:
            print('enter value greater than 0')
    
    def area(self):
        #calculate and return the area of a shape
        result = self.length * self.width
        return result
obj = Shape(2,3)
print(obj.area())

#changing a value directly
obj.length =  -3
print(obj.area())

#changing a value through an interface
obj.change_length(-3)

output

6
-9
enter value greater than 0

In the above example, the wrong input was supplied because the instance variables are publicly available through the instance.

If these variables are made hidden from the user, the user would have no choice but to use the interfaces defined – change_length() and change_width() to alter the values of the instance variables.

Name Mangling

Encapsulation ensures a clear separation between the interface and the implementation.

Python provides loose support for encapsulation as methods and attributes are public by default.

However, you make the attributes and methods something close to private using a concept known as name mangling.

Mangled names are sometimes referred to as private names. They are ways in which Python restricts access to the names outside of the class.

This is done using single or double-leading underscores on a name to restrict it from direct modification.

class Shape:
    def __init__(self, length, width):
        #define and assign instance variables as private
        self._length = length
        self._width =  width

    def change_length(self, new_length):
        #change the value of an instance variable
        if new_length > 0:
            self.length = new_length
        else:
            print('enter value greater than 0')
    def change_width(self, new_width):
        #change the value of an instance variable
        if new_width > 0:
            self.width = new_width
        else:
            print('enter value greater than 0')
    
    def area(self):
        result = self.length * self.width
        return result

obj.length =  -3

output

Traceback (most recent call last):
File "/Users/mac/Documents/ex.py", line 21, in
print(obj.area())
File "/Users/mac/Documents/ex.py", line 18, in area
result = self.length * self.width
AttributeError: Shape instance has no attribute 'length'

Inheritance

Inheritance is an aspect of object-oriented programming where classes or objects inherit the attributes and methods of other classes. This is particularly useful in situations where certain groups of objects share similar characteristics.

With inheritance, you don’t always have to write your classes from scratch. Instead of creating multiple classes, you can create a simple or generic class that the objects you want to model have in common. Then, you can create classes that inherit its attributes and behaviours.

The class which is inherited by another class is called the parent, super or base class, while the class inheriting from another class is called the child, sub or derived class.

Superclasses and subclasses

Subclasses or child classes contain all the attributes and methods defined in Superclasses.

Let’s say you want to create classes modelling different kinds of animals. Rather than creating individual classes for these animals, you can create a class consisting of the attributes and behaviours shared by all animals.

For instance, you can create a parent class called Animal and then create child classes such as Dog, Elephant, Cat and so on.

inheritance in object oriented programming

The parent class can have attributes such as name, age, gender and colour. It can also have methods such as move, make_sound, breath and so on. The child class can then provide attributes and behaviours that are peculiar to them.

The concept of inheritance

Inheritance is considered an is-a relationship. In line with the illustration above, a Dog is an Animal. This is a typical example of inheritance.

is a relationship
is a relationship

Now, let’s apply this concept to programming.

In Python, to indicate that a class is inheriting from another class, you should provide the name of the parent class in parenthesis immediately after the name of the class and before the colon. The example below is a class modelling an animal.


class Animal:
    def __init__(self, name, age, gender, colour):
        #define instance variables and assign them with values
        self.name = name
        self.age = age
        self.gender = gender
        self.colour = colour
    def move(self):
        #print moving
        print('moving')
    def make_sound(self):
        #print sound
        print('sound')
        
    def breathe(self): 
        #print breathing
        print('breathing')

There are basically two things you can do with inheritance and they include:

  • Extending
  • Overriding

Extending a class means providing additional methods or attributes in the child class while overriding means enhancing or specializing methods in a parent class.

Extending a class

The example below shows how the child class can extend the parent class by providing additional details that are not available in the parent class. Let’s create a class named Dog that will inherit and extend the class Animal.


class Dog(Animal):
    def __init__(self, name, age, gender, colour):
        #define the instance variables and assign them with values
        self.name = name
        self.age = age
        self.gender = gender
        self.colour = colour

    #extending the parent class
    def run(self):
        #print running
        print('running')

#instantiate the object
dog = Dog('Jacky', 3, 'Male', 'Brown')

#accessing the methods of the parent class
dog.move()
dog.make_sound()
dog.breathe()

#Accessing the run() method defined in the child class
dog.run()


output

moving
sound
sound
running

From the output of the above code, you can see the Dog class inherited all the attributes of the parent class Animal. It also extended the parent class by defining an additional method run() which is not defined in the parent class.

Overriding a class

Inheritance enables the child class to specialize or enhance methods contained in the parent class. In Python, this is done by writing methods with the same name as in the parent class.

The example below shows how to override the methods defined in the parent class.


class Dog(Animal):
    def __init__(self, name, age, gender, colour):
        #define the instance variables and assign them with values
        self.name = name
        self.age = age
        self.gender = gender
        self.colour = colour

    #overidding the parent class
    def move(self):
        print('run run run...')
    def make_sound(self):
        print('bark bark bark..')


#instantiate the object
dog = Dog('Jacky', 3, 'Male', 'Brown')

#accessing the overridden methods
dog.move()
dog.make_sound()


output

run run run...
bark bark bark...

Types of inheritance

There are two types of inheritance and they include:

  • Single Inheritance
  • Multiple inheritance

For a single inheritance, the child class has only one parent class, but for multiple inheritance, the child class has more than one parent class that it is inheriting from.

Multiple Inheritance

This is a type of inheritance where a child class has more than one parent class. The child class inherits the attributes and methods of the parent classes.

Hence, if you try to access any of the attributes or methods, python searches all the superclasses following the method resolution order (MRO) until a match is found. MRO is also known as diamond patterns because it emphasizes breadth or across before moving up.

The example below is a typical example of multiple inheritance.

class Shape:
    def __init__(self):
        #define and assign value to the instance variable
        self.sides = 1

    def get_name(self, sides):
        #return the name of a shape
        self.sides = sides
        if self.sides == 3:
            return 'Triangle'
        elif self.sides == 4:
            return 'Rectangle'
        elif self.sides == 5:
            return 'Pentagon'
        else:
            return 'shape'
    
class Colour:
    def __init__(self):
        self.colour = 'colour'

    def get_black(self):
        #return black
        self.colour = 'black'
        return self.colour

    def get_blue(self):
        #return blue
        self.colour = 'blue'
        return self.colour

    def get_green(self):
        #return green
        self.colour = 'green'
        return self.colour


class Drawing(Shape, Colour):
    def __init__(self, name):
        self.name = name

#instantiating the object
obj = Drawing('Tree')

#Acessing methods of the Colour class
result = obj.get_black()
print(result)

result = obj.get_green()
print(result)

#Accessing a method in the Shape class
result = obj.get_name(3)
print(result)

output

black
green
Triangle

In the example above, the Drawing class inherits from the Shape and Colour classes. This is multiple inheritance, and by virtue of inheritance, it has access to all the attributes and methods of the parent classes.

Composition

Consider an object like the computer, which is made up of many components or objects.

A typical computer is made up of objects such as keyboards, speakers, microphones, processors, etc. These objects form computers the computer system or unit.

To manufacture a computer, you don’t necessarily have to build all these parts on your own. Simply buy the individual components from different vendors and assemble them together.

To model an object such as a computer using the principles of object-oriented programming, you can apply the same principle. Simply write classes representing these components (objects) and assemble their objects together as a single unit.

This approach to programming is known as composition.

It is a way of writing programs where different objects are combined together to form a single unit. Inside this unit, the individual objects interact with each other in performing tasks.

The relationship between a composite object (computer) and the individual objects that makes it up is regarded as has-a relationship. For example, a computer has a keyboard.

composition in object oriented programming
has a relationship

To demonstrate this, let’s create a class called Computer, consisting of objects such as processors and hard disks.

class Processor:
    def __init__(self, manufacturer, speed):
        #assign values to the instance variables
        self.manufacturer = manufacturer
        self.speed = speed
    def get_manufacturer(self):
        #return the manufacturer
        return self.manufacturer
    def get_speed(self):
        #return the speed
        return self.speed

    def boot(self):
        #boot the processor
        print('booting...')

Below is the class for the HardDisk:

class HardDisk:
    def __init__(self, capacity):
        self.capacity = capacity

    def get_capacity(self):
        #return capacity
        return self.capacity

Now, let’s write a class named Computer.

class Computer:
    def __init__(self, make, processor, hdd):
        #assign values to the instance variables
        self.make = make
        self.processor = processor
        self.hdd = hdd
    def boot_computer(self):
        #call the boot method of the Processor class
        self.processor.boot()

    def get_hdd_size(self):
        #call the get_capacity method of the HardDisk class
        print('Hard disk size: ' + self.hdd.get_capacity())
    def get_processor_speed(self):
        #call the get_speed method of the processor
        print('Processor speed: ' + self.processor.get_speed())

#instantiating the Processor object
processor = Processor('Intel', '1.8GHz')

#instantiating the HardDisk object
hdd = HardDisk('500GB')

#instantiating the Computer object
comp = Computer('HP', processor, hdd)

#Accessing the methods of the Computer Class
comp.boot_computer()
comp.get_processor_speed()
comp.get_hdd_size()

output

booting...
Processor speed: 1.8GHz
Hard disk size: 500GB

Polymorphism

Polymorphism is derived from the Greek words Poly and Morphs meaning many shapes. In object-oriented programming, polymorphism means that a method or object can be or perform different operations in different contexts.

Polymorphism is often implemented where there is inheritance, and this is shown in the example below:


class Shape:
    def __init__(self):
        self.name = 'Shape'
    def draw(self):
        #return the name of the shape
        print('drawing ' + self.name)

class Triangle(Shape):
    def __init__(self):
        #assign instance variable to a new value
        self.name = 'Triangle'

class Rectangle(Shape):
    def __init__(self):
        #assign instance variable to a new value
        self.name = 'Rectangle'

#instatiating the objects

shape =  Shape()
triangle = Triangle()
rectangle = Rectangle()

#drawing shapes using polymorphism
shape.draw()
triangle.draw()
rectangle.draw()

output

drawing a shape
drawing a triangle
drawing a rectangle

In the above example, the Shape class defined a method draw(). This method is inherited and overridden by the Triangle and Rectangle classes.

Invoking the draw() method produces different outcomes depending on which object it is invoked upon.

Polymorphism can also be implemented through abstract classes where you define a method that does nothing and allows the child or derived classes to provide the implementations.


class Shape:
    def __init__(self):
        #define and assign value to the instance variable
        self.name = 'Shape'
    def draw(self):
        #do nothing
        pass

class Triangle(Shape):
    def __init__(self):
        #assign instance variable to a new value
        self.name = 'Triangle'

    def draw(self):
        #return the name of the shape
        print('drawing ' + self.name)

class Rectangle(Shape):
    def __init__(self):
        #assign instance variable to a new value
        self.name = 'Rectangle'

    def draw(self):
        #return the name of the shape
        print('drawing ' + self.name)

#instatiating the objects

triangle = Triangle()
rectangle = Rectangle()

#drawing shapes using polymorphism
triangle.draw()
rectangle.draw()

output

drawing a triangle
drawing a rectangle

Operator Overloading

In Python, everything is an object. You have seen how to create and instantiate classes with Python. You have also seen how to use inheritance and composition to break down complex problems into smaller and specialized classes.

Objects can be classified into two categories:

  • Built-in types
  • User-defined types

The built-in types are built into the Python interpreter and they include types such as integers, strings, tuples, dictionaries, sets and lists. The user-defined types are derived from classes or objects written by programmers.

Imagine that you created an object called a building and you want to perform operations such as addition or subtraction on it. You may wish to compare two or more buildings to find out which is greater or less than the other.

To do this, a technique known as operator overloading is used.

Operator overloading is a way of giving meaning to operators in Python.

Consider the example below:


#performing additions
num1 = 2
num2 = 3
print(num1+num2)

L1 = [1,2,3]
L2 = [5,6,7]
print(L1+L2)

#performing multiplication
print(num1 * 2)
print(L1*2)
print('hello'*4)

output

5
[1, 2, 3, 5, 6, 7]
4
[1, 2, 3, 1, 2, 3]
hellohellohellohello

In the above example, the operator + performs addition on numbers but joins or concatenates sequences such as lists and strings. Also, the operator * performs multiplication on numbers but performs repetition on sequences.

With operator overloading, you can define how certain operators behave with respect to the objects of your classes.

The illustration below shows some of the common operator overloading in python

operator overloading
Common operator overloading in Python

In Python, methods with double underscores (__) at the beginning and end of it are considered special methods. Operator overloading methods start with two double underscores to keep them distinct from other names in the class.

So, if you want your classes to support any of the given operators, you have included the operator overloading in your class.

Printing an instance of a class

The example below is a class named Car. If you print the instance of this class, this is what you will get.

class Car:
    def __init__(self, name):
        #initializing the instance variable
        self.name = name

    def get_name(self):
        #return the instance variable name
        return self.name

#instantiating the class
obj = Car('Tesla')
print(obj)

output

<__main__.Car instance at 0x10b02dcf>

Of course, this is not the outcome that you desire. If you want your object to support printing or to be converted to a string, then you have to implement the __str__() method in your class.

__str__() method

This method allows an instance to be printed or converted to a string. Now, let’s use operator overloading to instruct the Python interpreter what to do whenever the object is printed.

class Car:
    def __init__(self, name):
        #initialize the instance variable
        self.name = name

    def get_name(self):
        #return the instance variable
        return self.name

    def __str__(self):
        #result when the instance is printed
        return self.get_name()

#instantiating the class
obj = Car('Tesla')

#print object
print(obj)

output

Tesla

Overloading arithmetic operators

Supposed you want to perform arithmetic operations on instances of a class. Then, you have to overload the operators for the operations. The example below is a class that overloads arithmetic operators like +, -, * and /.

class Square:
    def __init__(self, num):
        #define and assign value to the instance variable
        self.num = num**2

    def __add__(self, val):
        #addition
        return self.num + val

    def __sub__(self, val):
        #subtraction
        return self.num - val

    def __mul__(self, val):
        #multiplication
        return self.num * val

    def __div__(self, val):
        #division
        return (float(self.num) / val)

#instantiating object
obj = Square(3)
num = 2

#addition
result = obj + num
print(result)

#subtraction
result = obj - num
print(result)

#multiplication
result = obj * num
print(result)

#division
result = obj / num
print(result)

output

11
7
18
4.5

However, if you perform these operations in a reversed order, you will get an error.


obj = Square(3)

result = 4 + obj

print(result)

output

Traceback (most recent call last):
  File "/Users/mac/Documents/ex.py", line 42, in >
    result = num + obj 
TypeError: unsupported operand type(s) for +: 'int' and 'instance'

This is because these methods consider the operand on the left side as the object of its class. Switching the position of the instance will result in an error. To handle this issue, use the complementary methods, __radd__(), __rsub__(), __rmul__() and __rdiv__() as shown below:

class Square:
    def __init__(self, num):
        #define and assign value to the instance variable
        self.num = num**2

    def __add__(self, val):
        #left addition
        return self.num + val

    def __radd__(self, val):
        #right addition
        return val + self.num

    def __sub__(self, val):
        #left subtraction
        return self.num - val

    def __rsub__(self, val):
        #right subtraction
        return val - self.num

    def __mul__(self, val):
        #left multiplication
        return self.num * val
    
    def __rmul__(self, val):
        #right multiplication
        return val * self.num

    def __div__(self, val):
        #left division
        return (float(self.num) / val)

    def __rdiv__(self, val):
        #right division
        return (val / float(self.num))

#instantiating object
obj = Square(3)
num = 2

#addition
result = obj + num
print(result)

result = num + obj
print(result)

#subtraction
result = obj - num
print(result)

result = num - obj
print(result)

#multiplication
result = obj * num
print(result)

result = num * obj
print(result)

#division
result = obj / num
print(result)

result = num / obj
print(result)

output

11
11
7
-7
18
18
4.5
0.222222222222

Comparing values

Let’s say that you want to compare two or more instances of a class using comparison operators like > (greater than) or < (less than). You can also use operator overloading to determine how instances are compared.

You can determine which instance is greater, less or equal to each other as shown in the example below:

class Square:
    def __init__(self, num):
        self.num = num**2

    def __gt__(self, val):
        #greater than
        result = False
        if self.num > val:
            result = True
        return result

    def __lt__(self, val):
        #less than
        result = False
        if self.num < val:
            result = True
        return result

#instantiating object
obj = Square(3)

print(obj > 5)
print(obj < 5)

output

True
False

Like the arithmetic operators, comparative operators also have complementary methods including __rlt__(), __rgt__(), __rle__(), __rge__(), __req__() and so on.

Operator overloading also works on logical operators such as and, or as well as bitwise operators such as left shift, right shift and so on.

Testing Classes

One of the characteristics of a good program is the ability to produce correct results in a consistent manner. Let’s say that you have a program that takes a sequence of numbers as input and produces the sum of the numbers as output.

It is expected of it to produce correct results regardless of the number of items in the sequence or the type of numbers (integers, floats or complex numbers) in question.

In essence, a good program produces the expected results at all times.

To ensure that your program is consistent in producing expected results, testing is needed. You can do this manually by supplying it with a random set of inputs and checking whether the outputs are in line with the expected results.

This is not only tiring but error-prone. A better alternative would be to automate the process using testing tools available in Python.

There are so many tools available for testing in Python, for a start, you can use the unittest module that comes with the standard installations of Python.

Testing a class

It’s good practice to write tests for any method or function in your program. Testing ensures that your programs are not only correct but free from bugs.

In test-driven developments (TDD), you are expected to write a test for every piece of code that you write.

One of the most effective ways you can test a class is by testing the individual components that make up the class. Typically, a class consists of methods and attributes.

Of course, methods represent the behaviour of a class and are regarded as the smallest unit of a class. Testing whether each of these units (methods) performs as expected is known as unit testing.

So, if you are to test a class, you might consider writing a series of unit tests, otherwise known as test cases to ensure that the methods perform as expected.

Keep in mind that you don’t have to perform tests on all your methods. However, it is a good practice to perform unit tests on methods that are critical to the performance of your program as a whole.

Let’s say that you want to carry out a test on the class below:

class Number:
    def __init__(self, val):
        #define and assign values to the instance variable
        self.value = val

    def add(self, val):
        #perform addition and result result
        result = self.value + val
        return result
    
    def subtract(self, val):
        #perform subtraction and return result
        result =  self.value - val
        return result
    
    def multiply(self, val):
        #perform multiplication and return result
        result = self.value * val
        return result

    def divide(self, val):
        #perform division and return result
        result = self.value / val
        return result

Now, let’s write the test cases.


import unittest
class NumberTestCase(unittest.TestCase):
    def test_add(self):
        #testing the add method
        number =  Number(10)
        val = 10
        output = 20
        result = number.add(val)
        self.assertEqual(result, output)

    def test_subtract(self):
        #testing the subtract method
        number =  Number(10)
        val = 10
        output = 0
        result = number.subtract(val)
        self.assertEqual(result, output)

    def test_multiply(self):
        #testing the multiply method
        number =  Number(10)
        val = 10
        output = 100
        result = number.multiply(val)
        self.assertEqual(result, output)

    def test_divide(self):
        #testing the divide method
        number =  Number(10)
        val = 10
        output = 1
        result = number.divide(val)
        self.assertEqual(result, output)

#running the unit tests
unittest.main()

To run the tests, call on the main() method in the unittest to execute the tests. This is the output you will get from your terminal.

unit testing
screenshot for the unit test

Conclusion

Object-oriented programming is a powerful way of writing efficient and robust programs. It offers an efficient way to structure your programs, making them easy for reuse and modification.

The idea of object-oriented programming involves breaking down your programs into smaller and self-contained units known as objects. It’s a way of programming where you build your programs, component by component or object by object. These objects are eventually coupled together to form a complete system.

You’ve learned about classes and how they are used as templates for creating objects. You have also learnt various aspects of object-oriented programming such as encapsulation, inheritance, composition and polymorphism.

As much as object-oriented programming is considered optional by some programmers, if you are really serious about programming and not just learning it for fun, OOP is compulsory.

 

Leave a Reply

Your email address will not be published. Required fields are marked *