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.
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.
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.
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.
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.
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
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.
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.