In procedural programming, a program is divided into smaller parts called methods. These methods are the basic entities used to construct a program. One of the main advantages of procedural programming is code reusability. However, the implementation of a complex real-world scenario becomes a difficult and unwieldy task.
Object-oriented programming, also referred to as OOP, is a programming paradigm that includes, or relies on, the concept of classes and objects.
Programming isn't much use if you can't model real-world scenarios using code, right? This is where OOP comes.
The basic idea of OOP is to divide a sophisticated program into a number of objects that talk to each other. Objects in a program frequently represent real-world objects.
It is also possible for objects to serve application logic and have no direct, real-world parallels. They manage things like authentication, templating, request handling, or any of the other myriad features needed for a practical application.
Objects may contain data in the form of fields (variables) and methods to operate on that data. Take the example of a real-world light bulb. It has a state (on or off), it has a behaviour, which means that when it is turned on it lights up, and when it is turned off, it does not produce any light. To conclude this, one can say:
Objects are a collection of data and their behaviours.
But where do these objects come from? The answer is classes.
A class can be thought of as a blueprint for creating objects.
The illustration below shows what a LightBulb class should look like:
From the above illustration, you can see that the state of the object is generally modeled using variables in a class, and the behaviour is modeled using methods. There can be different objects of the same class. Each can be in an independent state, but all of them will share the same behaviour and characteristics.
It can be inferred from the discussion above that classes are user-defined data types implemented using primitive data types. While primitive data types only focus on modeling the state of the object, user-defined data types can encapsulate the state and its behaviours into a unit.
We see objects everywhere. These objects have certain properties that define them. There are certain behaviours that these objects perform on their own, and there are actions that can be performed on them. Let's take the example of a company employee. An employee has the following properties or attributes:
- ID
- Salary
- Department
The following actions or behaviours can be performed on an employee:
- Calculation of tax on salary
- Calculation of salary per day
In a company, each worker has a different name, salary, and department, but the type of each worker is employee. So there is a generic blueprint for each worker, but each of them has different attributes.
A class has a singular blueprint, and objects are part of a class and are differentiated by their distinct properties.
Properties are variables that contain information regarding the object of a class. An employee object will have an ID, salary and department as its properties.
Attributes are also referred to as properties or members. For consistency, we will be using properties throughout the course.
Methods are like functions that have access to properties (and other methods) of a class. They can accept parameters and return values. They are used to perform an action on an object of a class. In the example above, we would have tax() and salaryPerDay() as class methods.
Behaviours are also referred to as member functions or methods. For consistency, we will be using methods throughout the course.
They allow us to create complex applications in Python. This is why they're considered the building blocks of OOP principles.
- Objects and classes are also instrumental for compartmentalizing code. Different components can become separate classes that would interact through interfaces. These ready-made components will also be available for use in future applications.
- The use of classes makes it easier to maintain different parts of an application since it is easier to make changes in classes.
In Python, classes are define as follows:
class ClassName:
passThe class keyword tells the compiler that we are creating a custom class, which is followed by the class name and the : sign. All the properties and methods of the class will be defined withing the class scope.
- Must start with a letter or underscore
- Should only be comprised of numbers, letters, or underscores
Suppose a class MyClass. We can create an object of a class by simply using the name of the class followed by a pair of parenthesis. It looks similar to calling a function, but Python can distinguish between the two and creates a new object of corresponding class. Example:
class MyClass:
pass
obj = MyClass()
print(obj)Output:
<__main__.MyClass object at 0x7efd1d626978>
By printing the object, obj, the output is the memory address at which this object is stored.
Let's implement a class Employee in Python. We'll start with just adding the properties of the class and will later extend it by adding methods in it.
class Employee:
ID = None
salary = None
department = NoneNote that if you do not initialize the values of properties, the Python code will not compile. Initializing the values of properties inside the class is necessary.
To access properties of an object, the dot notation is used:
object.propertyThere are two ways to assign values to properties of a class.
- Assign values when defining the class.
class Employee:
# defining the properties and assigning values to them
ID = 3789
salary = 2500
department = "Human Resources"
# creating an object of the Employee class
Steve = Employee()
# printing properties of Steve - an object of the Employee class
print("ID =", Steve.ID)
print("Salary", Steve.salary)
print("Department:", Steve.department)- Assign values in the main code.
class Employee:
# defining the properties and assigning them None
ID = None
salary = None
department = None
# creating an object of the Employee class
Steve = Employee()
# assigning values to properties of Steve - an object of the Employee class
Steve.ID = 3789
Steve.salary = 2500
Steve.department = "Human Resources"
# Printing properties of Steve
print("ID =", Steve.ID)
print("Salary", Steve.salary)
print("Department:", Steve.department)Python provides a feature to create properties of an object outside the class. Let's see an example of this by extending the example of Employee class above.
class Employee:
# defining the properties and assigning them None
ID = None
salary = None
department = None
# creating an object of the Employee class
Steve = Employee()
# assigning values to properties of Steve - an object of the Employee class
Steve.ID = 3789
Steve.salary = 2500
Steve.department = "Human Resources"
# creating a new attribute for Steve
Steve.title = "Manager"
# Printing properties of Steve
print("ID =", Steve.ID)
print("Salary", Steve.salary)
print("Department:", Steve.department)
print("Title:", Steve.title)The property title will only be added to Steve and all other future objects will only have the properties which are declared in the class.
To initialize an object of a class we use an initializer. It is a special method that outlines the steps that are performed when an object of a class is created in the program. It's used to define and assign values to instance variables (discussed after this section). The initialization method is similar to other methods but has a predefined name, __init__
The double underscores mean that is a special method that the Python interpreter will treat as a special case.
The initializer is a special method because it does not have a return type. The first parameter of __init__ is self, which is a way to refer to the object being initialized. It is always a good practice to define it as the first member method in the class definition.
Initializers are called when an object of a class is created. Example:
class Employee:
def __init__(self, ID, salary, department):
self.ID = ID
self.salary = salary
self.department = department
Steve = Employee(3789, 2500, "Human Resources")
print("ID :", Steve.ID)
print("Salary:", Steve.salary)
print("Department:", Steve.department)Similar to methods, we can also define initializers with optional parameters. As discussed previously, it's necessary to assign initial values to class properties when defining the class. So, when defining an initializer with optional parameters, it's essential to assign default values to the properties.
You can also have a default initializer that has all properties as optional. In this case, all the new objects will be created using the properties initialized in the initializer definition.
Below is an example where one Employee class object is created without the initializer parameters and one is created with the initializer parameters.
class Employee:
# defining the properties and assigning None to them
def __init__(self, ID=None, salary=0, department=None):
self.ID = ID
self.salary = salary
self.department = department
# creating an object of the Employee class with default parameters
Steve = Employee()
Mark = Employee("3789", 2500, "Human Resources")
# Printing properties of Steve and Mark
print("Steve")
print("ID :", Steve.ID)
print("Salary :", Steve.salary)
print("Department :", Steve.department)
print("Mark")
print("ID :", Mark.ID)
print("Salary :", Mark.salary)
print("Department :", Mark.department)In Python, properties can be defined into two parts:
- class variables
- instance variables
The class variables are shared by all instances or objects of the classes. A change in the class variable will change the value of that property in all the objects of the class.
The instance variables are unique to each instance or object of the class. A change in the instance variable will change the value of the property in that specific object only.
Class variables are defined outside the initializer and instance variables are defined inside the initializer.
class Player:
teamName = 'Liverpool' # class variables
def __init__(self, name):
self.name = name # creating instance variables
p1 = Player('Mark')
p2 = Player('Steve')
print("Name:", p1.name)
print("Team Name:", p1.teamName)
print("Name:", p2.name)
print("Team Name:", p2.teamName) Output:
Name: Mark
Team Name: Liverpool
Name: Steve
Team Name: Liverpool
Class variables are useful when implementing properties that should be common and accessible to all class objects. Let's see an example of this:
class Player:
teamName = 'Liverpool' # class variables
teamMembers = []
def __init__(self, name):
self.name = name # creating instance variables
self.formerTeams = []
self.teamMembers.append(self.name)
p1 = Player('Mark')
p2 = Player('Steve')
print("Name:", p1.name)
print("Team Members:")
print(p1.teamMembers)
print("")
print("Name:", p2.name)
print("Team Members:")
print(p2.teamMembers)Output:
Name: Mark
Team Members:
['Mark', 'Steve']
Name: Steve
Team Members:
['Mark', 'Steve']
There are three types of methods in Python:
- instance methods
- class methods
- static methods
Instance methods are the most commonly used in Python, so the term methods apply to them. Class methods and static methods will be named explicitly as they are.
Methods act as an interface between a program and the properties of a class in the program. These methods can either alter the content of the properties or use their values to perform a particular computation.
A method is a group of statements that performs some operations and may or may not return a result.
We will extend the Employee class example by adding methods to it.
Method parameters make it possible to pass values to the method. In Python, the first parameter of the method should ALWAYS be self and which followed by the remaining parameters.
The return statement makes it possible to get the value from the method. It must be immediately followed by the return value.
One of the major differences between functions and methods in Python is the first argument in the method definition. Conventionally, this is named self.
This pseudo-variable provides a reference to the calling object, that is the object to which the method or property belongs to. If the user does not mention the self as the first argument, the first parameter will be treated for reference to the object.
The self argument only needs to be passed in the method definition and not when the method is called.
Given below is an example of implementing methods in a class:
class Employee:
def __init__(self, ID=None, salary=None, department=None):
self.ID = ID
self.salary = salary
self.department = department
def tax(self):
return (self.salary * 0.2)
def salaryPerDay(self):
return (self.salary / 30)
Steve = Employee(3789, 2500, "Human Resources")
print("ID =", Steve.ID)
print("Salary", Steve.salary)
print("Department:", Steve.department)
print("Tax paid by Steve:", Steve.tax())
print("Salary per day of Steve", Steve.salaryPerDay())Output:
ID = 3789
Salary 2500
Department: Human Resources
Tax paid by Steve: 500.0
Salary per day of Steve 83.33333333333333
Overloading referes to making a method perform different operations based on the nature of its arguments.
Unlike in other programming languages, methods cannot be explicitly overloaded in Python but can be implicitly overloaded.
In order to include optional arguments, we assign default values to those arguments rather than creating a duplicate method with the same name. If the user chooses not to assign a value to the optional parameter, a default value will automatically be assigned to the variable.
class Employee:
# defining the properties and assigning them None to the
def __init__(self, ID=None, salary=None, department=None):
self.ID = ID
self.salary = salary
self.department = department
# method overloading
def demo(self, a, b, c, d=5, e=None):
print("a =", a)
print("b =", b)
print("c =", c)
print("d =", d)
print("e =", e)
def tax(self, title=None):
return (self.salary * 0.2)
def salaryPerDay(self):
return (self.salary / 30)
# creating an object of the Employee class
Steve = Employee()
# Printing properties of Steve
print("Demo 1")
Steve.demo(1, 2, 3)
print("\n")
print("Demo 2")
Steve.demo(1, 2, 3, 4)
print("\n")
print("Demo 3")
Steve.demo(1, 2, 3, 4, 5)Output:
Demo 1
a = 1
b = 2
c = 3
d = 5
e = None
Demo 2
a = 1
b = 2
c = 3
d = 4
e = None
Demo 3
a = 1
b = 2
c = 3
d = 4
e = 5
One might wonder that we could have simply created new methods to perform different jobs rather than overloading the same method. However, under the hood, overloading saves us memory in the system.
An obvious benefit is that the code becomes simple and clean. We don't have to keep track of different methods.
Polymorphism is a very important concept in object-oriented-programming. Method overloading plays a vital role in its implementation. The concept will come up later on in the course.
Class methods work with class variables and are accessible using the class name rather than its object. Since all class objects share the class variables, class methods are used to access and modify class variables.
To declare a method as a class method, we use the decorator @classmethod. cls is used to refer to the class just like self is used to refer to the object of the class. Just like instance methods, all class methods have at least one argument cls.
class MyClass:
classVariable = 'educative'
@classmethod
def demo(cls):
return cls.classVariable
Static methods are methods that are usually limited to class only and not their objects. They have no direct relation to class variables or instance variables. They are used as utility functions inside the class or when we do not want the inherited classes (more on this later) to modify a method definition.
They can be accessed using the class name or the object name.
To declare a method as a static method, we used the decorator @staticmethod. It does not use a reference to the object or class, so we do not have to use self or cls. We can pass as many arguments as we want and use this method to perform any function without interfering with the instance or class variables.
class MyClass:
@staticmethod
def demo():
print("I am a static method")Static methods do not know anything about the state of the class, i.e., they cannot modify class attributes. The purpose of a static method is to use its parameters and produce a useful result.
Suppose that there is a class, BodyInfo, containing information about the physical attributes of a person. We can make a static method for calculating the BMI of any given weight and height:
class BodyInfo:
@staticmethod
def bmi(weight, height):
return weight / (height**2)
weight = 75
height = 1.8
print(BodyInfo.bmi(weight, height))In Python, we can impose access restrictions on different data members and member functions. The restrictions are specified through access modifiers. Access modifiers are tags we can associate with each member to define which parts of the program can access it directly.
Public attributes are those that can be accessed inside the class and outside the class. By default, all methods and properties in a class are publicly available. If we want to suggest that a method should not be used publicly, we have to declare it as private explicitly.
Example:
class Employee:
def __init__(self, ID, salary):
# all properties are public
self.ID = ID
self.salary = salary
def displayID(self):
print("ID:", self.ID)
Steve = Employee(3789, 2500)
Steve.displayID()
print(Steve.salary)In the code above, the properties ID and salary and the method displayID() are public as they can be accessed in the class as well as outside the class.
Private attributes cannot be accessed directly from outside the class but can be accessed from inside the class.
The aim is to keep it hidden from the users and other classes. Unlike in different languages, it is not a widespread practice in Python to keep the data members private since we do not want to create hindrances for the users. We can make members private using the double underscore __ prefix. Trying to access private attributes in the main code will generate an error. An example is shown below.
class Employee:
def __init__(self, ID, salary):
self.ID = ID
self.__salary = salary # salary is a private property
Steve = Employee(3789, 2500)
print("ID:", Steve.ID)
print("Salary:", Steve.__salary) # this will cause an error- ID is a public property, but __salary is a private property, so it cannot be accessed outside the class
- When it is tried to be accessed outside the class, the following error is generated
'Employee' object has no attribute '__salary'
- To ensure that no one from the outside knows about this private property, the error does not reveal its identity.
class Employee:
def __init__(self, ID, salary):
self.ID = ID
self.__salary = salary # salary is a private property
def displaySalary(self): # displaySalary is a public method
print("Salary:", self.__salary)
def __displayID(self): # displayID is a private method
print("ID:", self.ID)
Steve = Employee(3789, 2500)
Steve.displaySalary()
Steve.__displayID() # this will generate an error- ID is a public property, so it can be accessed from outside and inside the class.
- __salary is a private property, so it cannot be accessed outside the class but can be accessed from inside the class.
- The displaySalary() method is a public method, so it can be accessed from outside the class. This method can access the private property, __salary, as well.
- The __displayID() method is a private method, so it cannot be accessed from outside the class.
- When you try to access displayID() from outside the class, the following error is generated:
'Employee' object has no attribute '__displayID()'
- To ensure that no one from the outside knows about this private property, the error does not reveal its identity.
As discussed above, it is not common to have private variables in Python.
Properties and methods with the double underscore prefix are usually present to make sure that the user does not carelessly access them. Python allows for free hand to the user to avoid any future complications in the code. If the user believes it is absolutely necessary to access a private property or a method, they can access it using the _ClassName prefix for the property or method. An example of this is shown below:
class Employee:
def __init__(self, ID, salary):
self.ID = ID
self.__salary = salary # salary is a private property
Steve = Employee(3789, 2500)
print(Steve._Employee__salary) # accessing a private property
Protected properties and methods in other languages can be accessed by classes and their subclasses, which will be discussed later in the course. As we have seen, Python does not have a strict rule for accessing properties and methods, so it does not have the protected access modifier.
Information hiding refers to the concept of hiding the inner workings of a class and simply providing an interface through which the outside world can interact with the class without knowing what's going on inside.
Take the doctor-patient model. In the case of an illness, the patient consults the doctor, after which he or she is prescribed and appropriate medicine. The patient only knows the process of going to the doctor. The logic and reasoning behind the doctor's prescription are unknown to the patient. This is a classic example of the patient class interacting with the doctor class without knowing the inner workings of the doctor class.
- Encapsulation
- Abstraction
Encapsulation is a fundamental programming technique used to achieve data hiding in OOP. It refers to binding data and the methods to manipulate that data together in a single unit, that is, class.
A class can be thought of as a capsule having methods and properties inside it. When encapsulating classes, a good convention is to declare all variables of a class private. This will restrict direct access by the code outside that class.
Now, to allow the outside world to communicate with the class, one has to implement public methods. These methods are called getters and setters.
- classes make the code easy to change and maintain.
- properties to be hidden can be specified easily.
- we decide which outside classes or functions can access the class properties.
- A getter method allows reading a property's value.
- A setter method allows modifying a property's value.
It is a common convention to write the name of the corresponding member fields with the get or set command.
class User:
def __init__(self, username=None): # defining initializer
self.__username = username
def setUsername(self, x):
self.__username = x
def getUsername(self):
return (self.__username)
Steve = User('steve1')
print('Before setting:', Steve.getUsername())
Steve.setUsername('steve2')
print('After setting:', Steve.getUsername())Output:
Before setting: steve1
After setting: steve2
Remember: the goal is to prevent this bound data from any unwanted access by the code outside this class. Let's understand this by using an example of a very basic User class.
Consider that we are up for designing an application and are working on modeling the log in part of that application. We know that a user needs a username and a password to log into the app.
An elementary User class will be modeled as:
- having a property userName
- having a property password
- A method named login() to grant access
class User:
def __init__(self, userName=None, password=None):
self.userName = userName
self.password = password
def login(self, userName, password):
if ((self.userName.lower() == userName.lower())
and (self.password == password)):
print("Access Granted!")
else:
print("Invalid Credentials!")
Steve = User("Steve", "12345")
Steve.login("steve", "12345")
Steve.login("steve", "6789")
Steve.password = "6789"
Steve.login("steve", "6789")In the above coding example, we can observe that anyone can access, change, or print the password and userName fields directly from the main code. This is dangerous in the case of this User class because there is no encapsulation of the credentials of a user, which means anyone can access their account by manipulating the stored data. So, the above code does not follow good coding practices.
class User:
def __init__(self, userName=None, password=None):
self.__userName = userName
self.__password = password
def login(self, userName, password):
if ((self.__userName.lower() == userName.lower())
and (self.__password == password)):
print(
"Access Granted against username:",
self.__userName.lower(),
"and password:",
self.__password)
else:
print("Invalid Credentials!")
# created a new User object and stored the password and username
Steve = User("Steve", "12345")
Steve.login("steve", "12345") # Grants access because credentials are valid
# does not grant access since the credentials are invalid
Steve.login("steve", "6789")
Steve.__password # compilation error will occur due to this lineIf you comment the last line, the program will work.
We can observe that no one can access, change, or print the __password and __userName fields directly from the main code. This is a proper implementation of encapsulation.
- For encapsulating a class, all the properties should be private and any access to the properties should be through methods such as getters and setters.
Inheritance provides a way to create a new class from an existing class. The new class is a specialized version of the existing class such that it inherits all the non-private fields (variables) and methods of the existing class. The existing class is used as a starting point or as a base to create the new class.
- Square IS A shape
- Python IS A programming language
- Car IS A vehicle
So, we can conclude that we can build new classes by extending existing classes.
| Existing Class | Derived class |
|---|---|
| Shape | Square |
| Programming language | Python |
| Vehicle | Car |
The primary purpose of object-oriented programming is to enable a programmer to model the real-world objects using a programming language.
In Python, whenever we create a class, it is, by default, a subclass of the build-in Python object class. This makes it an excellent example of inheritance in Python. This class has very few properties and methods, but id does provide a strong basis for object-oriented programming in Python.
In inheritance, in order to create a new class based on an existing class, we use the following terminology:
- Parent class (super class or base class): this class allows the reuse of its public properties in another class.
- Child class (subclass or derived class): this class inherits or extends the superclass.
In Python, to implement inheritance, the syntax is quite similar to the basic class definition. The syntax is given below:
class ParentClass:
# attributes of the parent class
class ChildClass(ParentClass):
# attributes of the child classLet's take an example of a Vehicle class as the parent class and implement a Car class that will extend from this Vehicle class. Because a car IS A vehicle, the implementation of inheritance relation between these classes will stand valid.
class Vehicle:
def __init__(self, make, color, model):
self.make = make
self.color = color
self.model = model
def printDetails(self):
print("Manufacturer:", self.make)
print("Color:", self.color)
print("Model:", self.model)
class Car(Vehicle):
def __init__(self, make, color, model, doors):
# calling the constructor from parent class
Vehicle.__init__(self, make, color, model)
self.doors = doors
def printCarDetails(self):
self.printDetails()
print("Doors:", self.doors)
obj1 = Car("Suzuki", "Grey", "2015", 4)
obj1.printCarDetails()- In the code above, we have defined a parent class, Vehicle, and a child class, Car.
- Car inherits all the properties and methods of the Vehicle class and can access and modify them.
- For example, we have called the printDetails() method, which was actually defined in the Vehicle class in the printCarDetails() method.
The use of super() comes into play when we implement inheritance. It is used in a child class to refer to the parent class without explicitly naming it. It makes the code more manageable, and there is no need to know the name of the parent class to access its attributes.
Consider the fields name fuelCap defined inside a Vehicle class to keep track of the fuel capacity of a vehicle. Another class named Car extends from this Vehicle class. We declare a class property inside the Car class with the same name, i.e., fuelCap, but with a different value. Now, if we want to refer to the fuelCap field of the parent class inside the child class, we will have to use the super() function.
class Vehicle: # defining the parent class
fuelCap = 90
class Car(Vehicle): # defining the child class
fuelCap = 50
def display(self):
# accessing fuelCap from the Vehicle class using super()
print("Fuel cap from the Vehicle Class:", super().fuelCap)
# accessing fuelCap from the Car class using self
print("Fuel cap from the Car Class:", self.fuelCap)
obj1 = Car() # creating a car object
obj1.display() # calling the Car class method display()Just like properties, super() is also used with methods. Whenever a parent class and the immediate child class have any methods with the same name, we use super() to access the methods from the parent class inside the child class. Let's go through an example:
class Vehicle: # defining the parent class
def display(self): # defining display method in the parent class
print("I am from the Vehicle Class")
class Car(Vehicle): # defining the child class
# defining display method in the child class
def display(self):
super().display()
print("I am from the Car Class")
obj1 = Car() # creating a car object
obj1.display() # calling the Car class method display()Another essential use of the function super() is to call the initializer of the parent class from inside the initializer of the child class.
Below, we have two codes that perform the same way to show that the call to super() in a method or an initializer is not necessarily in the first line of the method.
class ParentClass():
def __init__(self, a, b):
self.a = a
self.b = b
class ChildClass(ParentClass):
def __init__(self, a, b, c):
super().__init__(a, b)
self.c = c
obj = ChildClass(1, 2, 3)
print(obj.a)
print(obj.b)
print(obj.c)class ParentClass():
def __init__(self, a, b):
self.a = a
self.b = b
class ChildClass(ParentClass):
def __init__(self, a, b, c):
self.c = c
super().__init__(a, b)
obj = ChildClass(1, 2, 3)
print(obj.a)
print(obj.b)
print(obj.c)Now, let's take a previous example and use super() to refer to the parent class:
class Vehicle:
def __init__(self, make, color, model):
self.make = make
self.color = color
self.model = model
def printDetails(self):
print("Manufacturer:", self.make)
print("Color:", self.color)
print("Model:", self.model)
class Car(Vehicle):
def __init__(self, make, color, model, doors):
Vehicle.__init__(self, make, color, model)
self.doors = doors
def printCarDetails(self):
self.printDetails()
print("Door:", self.doors)
obj1 = Car("Suzuki", "Grey", "2015", 4)
obj1.printCarDetails()class Vehicle:
def __init__(self, make, color, model):
self.make = make
self.color = color
self.model = model
def printDetails(self):
print("Manufacturer:", self.make)
print("Color:", self.color)
print("Model:", self.model)
class Car(Vehicle):
def __init__(self, make, color, model, doors):
super().__init__(make, color, model)
self.doors = doors
def printCarDetails(self):
self.printDetails()
print("Door:", self.doors)
obj1 = Car("Suzuki", "Grey", "2015", 4)
obj1.printCarDetails()Based upon parent classes and child classes, there exists the following five types of inheritance:
- Single
- Multi-level
- Hierarchical
- Multiple
- Hybrid
In single inheritance, there is only a single class extending from another class. We can take the example of the Vehicle class as the parent class, and the Car class as the child class.
class Vehicle: # parent class
def setTopSpeed(self, speed): # defining the set
self.topSpeed = speed
print("Top speed is set to", self.topSpeed)
class Car(Vehicle): # child class
def openTrunk(self):
print("Trunk is now open.")
corolla = Car() # creating an object of the Car class
corolla.setTopSpeed(220) # accessing methods from the parent class
corolla.openTrunk() # accessing method from its own classWhen a class is derived from a class which itself is derived from another class, it is called multilevel inheritance. We can extend the classes to as many levels as we want to.
Let’s implement the three classes illustrated above:
- A car IS A vehicle
- A hybrid IS A car
class Vehicle: # parent class
def setTopSpeed(self, speed): # defining the set
self.topSpeed = speed
print("Top speed is set to", self.topSpeed)
class Car(Vehicle): # child class of Vehicle
def openTrunk(self):
print("Trunk is now open.")
class Hybrid(Car): # child class of Car
def turnOnHybrid(self):
print("Hybrid mode is now switched on.")
priusPrime = Hybrid() # creating an object of the Hybrid class
priusPrime.setTopSpeed(220) # accessing methods from the parent class
priusPrime.openTrunk() # accessing method from the parent class
priusPrime.turnOnHybrid() # accessing method from the child classIn hierarchical inheritance, more than one class extends from the same base class. The common attributes of these child classes are implemented inside the base class.
Example:
- A car IS A vehicle
- A truck IS A vehicle
class Vehicle: # parent class
def setTopSpeed(self, speed): # defining the set
self.topSpeed = speed
print("Top speed is set to", self.topSpeed)
class Car(Vehicle): # child class of Vehicle
pass
class Truck(Vehicle): # child class of Vehicle
pass
corolla = Car() # creating an object of the Car class
corolla.setTopSpeed(220) # accessing methods from the parent class
volvo = Truck() # creating an object of the Truck class
volvo.setTopSpeed(180) # accessing methods from the parent classWhen a class is derived from more than one base class, i.e., when a class has more than one immediate parent class, it is called multiple inheritance.
Example:
- HybridEngine IS A ElectricEngine
- HybridEngine IS A CombustionEngine as well.
class CombustionEngine():
def setTankCapacity(self, tankCapacity):
self.tankCapacity = tankCapacity
class ElectricEngine():
def setChargeCapacity(self, chargeCapacity):
self.chargeCapacity = chargeCapacity
# Child class inherited from CombustionEngine and ElectricEngine
class HybridEngine(CombustionEngine, ElectricEngine):
def printDetails(self):
print("Tank Capacity:", self.tankCapacity)
print("Charge Capacity:", self.chargeCapacity)
car = HybridEngine()
car.setChargeCapacity("250 W")
car.setTankCapacity("20 Litres")
car.printDetails()A type of inheritance which is a combination of Multiple and Multi-level inheritance is called hybrid inheritance.
- CombustionEngine IS A Engine
- ElectricEngine IS A Engine
- HybridEngine IS A ElectricEngine and a CombustionEngine
class Engine: # Parent class
def setPower(self, power):
self.power = power
class CombustionEngine(Engine): # Child class inherited from Engine
def setTankCapacity(self, tankCapacity):
self.tankCapacity = tankCapacity
class ElectricEngine(Engine): # Child class inherited from Engine
def setChargeCapacity(self, chargeCapacity):
self.chargeCapacity = chargeCapacity
# Child class inherited from CombustionEngine and ElectricEngine
class HybridEngine(CombustionEngine, ElectricEngine):
def printDetails(self):
print("Power:", self.power)
print("Tank Capacity:", self.tankCapacity)
print("Charge Capacity:", self.chargeCapacity)
car = HybridEngine()
car.setPower("2000 CC")
car.setChargeCapacity("250 W")
car.setTankCapacity("20 Litres")
car.printDetails()It makes the code reusable. Consider that you are up for designing a banking system using classes. Your model might have these:
- A parent class: BankAccount
- A child class: SavingsAccount
- Another child class: CheckingAccount
In the above example, you don't need to duplicate the code for the deposit() and withdraw() methods inside the child classes.
Suppose you put the same code in different classes, but what happens when you have to make changes to a function and in several places? There is a high likelihood that you will forget some places and bugs will be introduced. You can avoid this with inheritance, which will ensure that all changes are localized, and inconsistencies are avoided.
It provides an easy way to upgrade or enhance specific parts of a product without changing the core attributes. An existing class can act as a base class from which a new class with upgraded features can be derived.
In the example above, you realize at a later point that you have to diversify this banking application by adding another class for MoneyMarketAccount. So, rather than implementing this class from scratch, you can extend it from the existing BankAccount class as a starting point. You can also reuse its attributes that are common with MoneyMarketAccount.
The base class can keep some data private so that the derived class cannot alter it.
The word polymorphism is a combination of two greek words, poly meaning many and morph meaning forms.
In programming, polymorphism refers to the same object exhibiting different forms and behaviours.
For example, take the Shape Class. The exact shape you choose can be anything. It can be a rectangle, a circle, a polygon, or a diamond. While these are all shapes, their properties are different. This is called polymorphism.
Assume there is a parent class named Shape and child classes: Rectangle, Circle, Polygon and Diamond.
Suppose your application will need methods to calculate the area of each specific shape. You could throw in separate methods in each class (for instance, getSquareArea(), getCircleArea(), etc). But this makes it harder to remember each method's name.
It would be better if all specific area calculation methods could be called getArea(). This can be achieved in OOP using polymorphism. The base class declares a function without providing an implementation. Each derived class inherits the function declaration and can provide its own implementation.
It cuts down the work of the developer. When the time comes to create more specific subclasses with certain unique attributes and behaviours, the developer can alter the code in the specific areas where the responses differ. All the other pieces of code can be left untouched.
Consider two shapes that are defined as classes: Rectangle and Circle. These classes contain the getArea() method that calculates the area for the respective shape depending on the values of their properties.
class Rectangle():
# initializer
def __init__(self, width=0, height=0):
self.width = width
self.height = height
self.sides = 4
# method to calculate Area
def getArea(self):
return (self.width * self.height)
class Circle():
# initializer
def __init__(self, radius=0):
self.radius = radius
self.sides = 0
# method to calculate Area
def getArea(self):
return (self.radius * self.radius * 3.142)
shapes = [Rectangle(6, 10), Circle(7)]
print("Sides of a rectangle are", str(shapes[0].sides))
print("Area of rectangle is:", str(shapes[0].getArea()))
print("Sides of a circle are", str(shapes[1].sides))
print("Area of circle is:", str(shapes[1].getArea()))Output:
Sides of a rectangle are 4
Area of rectangle is: 60
Sides of a circle are 0
Area of circle is: 153.958
We will be implementing the parent class first, and then the child classes.
class Shape:
def __init__(self):
self.sides = 0
def getArea(self):
pass# Rectangle IS A Shape with a specific width and height
class Rectangle(Shape):
# initializer
def __init__(self, width, height):
self.width = width
self.height = height
self.sides = 4
# method to calculate Area
def getArea(self):
return self.width * self.height# Circle IS A Shape with a specific radius
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
self.sides = 0
# method to calculate Area
def getArea(self):
return self.radius * self.radius * 3.142Note: I don't see the point yet. The code would work if the getArea() method wasn't defined in Shape class.
It is the process of redefining a parent class's method in a subclass. In the previous example, the Rectangle and Circle classes were overriding the getArea() method from the Shape class. In this case:
- the method in the parent class is called the overridden method.
- the methods in the child classes are called the overriding methods.
- The derived classes can give their own specific implementations to inherited methods without modifying the parent class methods.
- For any method, a child class can use the implementation in the parent class or make its own implementation.
- Method overriding needs inheritance, and there should be at least one derived class to implement it.
- The methods in the derived classes usually have a dissimilar implementation.
Operators in Python can be overloaded to operate in a certain user-defined way. Whenever an operator is used in Python, its corresponding method is invoked to perform its predefined function. For example, when the + operator is called, it invokes the special function __add__ in Python, but this operator acts differently for different data types. For example, the + operator adds the numbers when it is used between two int data types and merges two strings when it is used between string data types.
print(5 + 3)
print("money" + "maker")outputs
8
moneymaker
When a class is defined, its objects can interact with each other through the operators, but is necessary to define the behaviour of these operators through operator overloading.
We are going to implement a class that represents a complex number. It consists of a real part and an imaginary part.
When we add a complex number, the real part is added to the real part, and the imaginary part is added to the imaginary part.
Let's implement the complex number class and overload the + and - operators below:
class Com:
def __init__(self, real=0, imag = 0):
self.real = real
self.imag = imag
def __add__(self, other):
temp = Com(self.real + other.real, self.imag + other.imag)
return temp
def __sub__(self, other):
temp = Com(self.real - other.real, self.imag - other.imag)
return temp
obj1 = Com(3, 7)
obj2 = Com(2, 5)
obj3 = obj1 + obj2
obj4 = obj1 - obj2
print("real of obj3:", obj3.real)
print("imag of obj3:", obj3.imag)
print("real of obj4:", obj4.real)
print("imag of obj4:", obj4.imag)Output:
real of obj3: 5
imag of obj3: 12
real of obj4: 1
imag of obj4: 2
| Operators | Method |
|---|---|
| + | __add__ (self, other) |
| - | __sub__ (self, other) |
| / | __truediv__ (self, other) |
| * | __mul__ (self, other) |
| < | __lt__ (self, other) |
| > | __gt__ (self, other) |
| == | __eq__ (self, other) |
| Operators | Method |
|---|---|
| + | __add__ (self, other) |
| - | __sub__ (self, other) |
| / | __truediv__ (self, other) |
| * | __mul__ (self, other) |
| < | __lt__ (self, other) |
| > | __gt__ (self, other) |
| == | __eq__ (self, other) |
We say that if an object quacks like a duck, swims like a duck, eats like a duck or in short, acts like a duck, that object is a duck.
Duck typing extends the concept of dynamic typing in Python. Dynamic typing means that we can change the type of an object later in the code.
See the code below for a better understanding of dynamic typing in Python:
x = 5 # type of x is an integer
print(type(x))
x = "Educative" # type of x is now string
print(type(x))Output:
<class 'int'>
<class 'str'>
class Dog:
def Speak(self):
print("Woof woof")
class Cat:
def Speak(self):
print("Meow meow")
class AnimalSound:
def Sound(self, animal):
animal.Speak()
sound = AnimalSound()
dog = Dog()
cat = Cat()
sound.Sound(dog)
sound.Sound(cat)Output:
Woof woof
Meow meow
- The type of animal is not defined in the definition of the method Sound.
- Type of animal is determined when the method is called, so it does not matter which object type you are passing as a parameter in the Sound() method, what matters is that the Speak() method should be defined in all the classes whose objects are passed in the Sound() method.
- We can use any property or method of animal in the AnimalSound class as long as it is declared in that class.
Abstract base classes (ABC) define a set of methods and properties that a class must implement in order to be considered a duck-type instance of that class.
Let's look at an example to understand why we should use abstract base classes.
class Shape: # Shape is a child class of ABC
def area(self):
pass
def perimeter(self):
pass
class Square(Shape):
def __init__(self, length):
self.length = length
def area(self):
return self.length * self.length
def perimeter(self):
return 4 * self.length
shape = Shape()
square = Square(4)In the example above, you can see that an instance of Shape can be created even though an object from this class cannot stand on its own. Shape class should provide a blueprint for its child classes to implement methods in it. To prevent the user from making a Shape class object, we use abstract base classes.
To define an abstract base class, we use the abc module. The abstract base class is inherited from the built-in ABC class. We have to use the decorator @abstractmethod above the method that we want to declare as an abstract method.
from abc import ABC, abstractmethod
class ParentClass(ABC):
@abstractmethod
def method(self)
Example:
from abc import ABC, abstractmethod
class Shape(ABC): # Shape is a child class of ABC
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Square(Shape):
def __init__(self, length):
self.length = length
shape = Shape()
# this code will not compile since Shape has abstract methods without
# method definitions in itfrom abc import ABC, abstractmethod
class Shape(ABC): # Shape is a child class of ABC
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Square(Shape):
def __init__(self, length):
self.length = length
square = Square(4)
# this will code will not compile since abstract methods have not been
# defined in the child class, SquareAs you can see above, the code does not compile since we have not defined the abstract methods, area and perimeter, inside the parent class, Shape, or the child class, Square. Let’s do it and see what happens:
from abc import ABC, abstractmethod
class Shape(ABC): # Shape is a child class of ABC
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Square(Shape):
def __init__(self, length):
self.length = length
def area(self):
return (self.length * self.length)
def perimeter(self):
return (4 * self.length)
shape = Shape()
# this code will not compile since Shape has abstract methods without
# method definitions in itfrom abc import ABC, abstractmethod
class Shape(ABC): # Shape is a child class of ABC
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Square(Shape):
def __init__(self, length):
self.length = length
def area(self):
return (self.length * self.length)
def perimeter(self):
return (4 * self.length)
square = Square(4)
# this code will not generate an error since abstract methods have been
# defined in the child class, Square- Note: Methods with @abstractmethod decorators must be defined in the child class.
By using abstract base classes, we can control classes whose objects can or cannot be created.
The concepts of inheritance and polymorphism taught us how to create dependent classes out of a base class. While inheritance represents a relationship between classes, there are situations where there are relationships between objects.
There are three main class relationships we need to know. We have studied the IS A relation. Let's study the other two below.
In this relationship, one class object is a component of another class object. An instance of the component class can only be created inside the main class. In the example above, class B and class C have their own implementations, but their objects are only created once a class A object is created. Hence, part-of is a dependent relationship.
This is a slightly less concrete relationship between two classes. Class A and class B have a has-a relationship if one or both need the other's object to perform an operation, but both class objects can exist independently of each other.
In OOP, association is the common term for both the has-a and part-of relationships but is not limited to these. Two objects are in an association relationship is a generic statement, which means that we don't worry about the lifetime dependency between the objects.
Aggregation follow the has-a model. This creates a parent-child relationship between two classes, with one class owning the object of another.
In aggregation, the lifetime of the owned object does not depend on the lifetime of the owner.
The owner object could get deleted, but the owned object can continue to exist in the program. In aggregation, the parent only contains a reference to the child, which removes the child's dependency.
Let's take the example of people and their country of origin. Each person is associated with a country, but the country can exist without the person.
class Country:
def __init__(self, name=None, population=0):
self.name = name
self.population = population
def printDetails(self):
print("Country Name:", self.name)
print("Country Population", self.population)
class Person:
def __init__(self, name, country):
self.name = name
self.country = country
def printDetails(self):
print("Person Name:", self.name)
self.country.printDetails()
c = Country("Wales", 1500)
p = Person("Joe", c)
p.printDetails()
# deletes the object p
del p
print("")
c.printDetails()As we can see, the Country object c lives on even after we delete the Person object p. This creates a weaker relationship between the two classes.
Composition is the practice of accessing other class objects in your class. In such a scenario, the class which creates the object of the other class is known as the owner and is responsible for the lifetime of that object.
Composition relationships are part-of relationships where the part must constitute a segment of the whole object. We can achieve composition by adding smaller parts of other classes to make a complex unit.
A car is composed of an engine, tires, and doors. In this case, a Car owned these objects, so a Car is an owner class, and the tires, doors and engine are the owned classes.
class Engine:
def __init__(self, capacity=0):
self.capacity = capacity
def printDetails(self):
print("Engine Details:", self.capacity)
class Tires:
def __init__(self, tires=0):
self.tires = tires
def printDetails(self):
print("Number of tires:", self.tires)
class Doors:
def __init__(self, doors=0):
self.doors = doors
def printDetails(self):
print("Number of doors:", self.doors)
class Car:
def __init__(self, eng, tr, dr, color):
self.eObj = Engine(eng)
self.tObj = Tires(tr)
self.dObj = Doors(dr)
self.color = color
def printDetails(self):
self.eObj.printDetails()
self.tObj.printDetails()
self.dObj.printDetails()
print("Car color:", self.color)
car = Car(1600, 4, 2, "Grey")
car.printDetails()We have created a Car class which contains the objects of Engine, Tires, and Doors classes. Car class is responsible for their lifetime, i.e, when a Car dies, so does tire, engine and doors too.













