You are on page 1of 30

Chapter 3

Python Object
Oriented
Programming
 Everything in Python is an object.
 An object has a state and behaviors.
 To create an object, you define a class first. And then, from the class, you
can create one or more objects. The objects are instances of a class.
Define a class
To define a class, you use the class keyword followed by the class name. For
example, the following defines a Person class:

class Person:
pass

In Python, a class is a blueprint for creating objects. It defines the properties


(attributes) and behaviors (methods) that objects of that class will have. An object
is an instance of a class
Here's an example of a simple class definition in Python:
class Person:
pass
To create an object from the Person class, you use the class name followed by
parentheses (), like calling a function:
pers1=Person()
pers2=Person()
In this example, the pers1 is an instance of the Person class.
Adding and retrieving dynamic attributes of classes

 Define instance attributes / Adding attributes

Python is dynamic. It means that you can add an attribute to an instance of a class
dynamically at runtime.

For example, the following adds the name attribute to the person object:
pers1.name = 'Vijay'
pers1.age=23
pers2.name='sakshi'
pers2.age=21

 Get instance attributes / Retrieving attributes


print(pers1.name)
print(pers1.age)
print(pers2.name)
print(pers2.age)

Define constructor
In Python, the constructor method is a special method called __init__. It is
automatically called when an object of a class is created. The purpose of the
constructor is to initialize the object's attributes.
Here's an example demonstrating the use of a constructor in Python:
The following defines the Person class with two instance attributes name and age:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

# Creating an instance of the Person class


person1 = Person("Avinash", 30)

# Accessing the attributes of the object


print(person1.name) # Output: Avinash
print(person1.age) # Output: 30

In this example, __init__ is the constructor method of the Person class. It takes
three parameters: self, which is a reference to the instance being created, name,
and age. Inside the constructor, self.name and self.age are instance variables,
which are initialized with the values passed as arguments during the object
creation (person1 = Person("Avinash ", 30)).
The self parameter is required in Python class methods. It is a reference to the
current instance of the class and is used to access variables and methods
associated with the instance. When calling methods or accessing attributes within
the class, you use self to refer to the instance itself.

Define instance methods


In Python, an instance method is a function defined within a class that operates
on instances (objects) of that class. Instance methods are typically used to access
or modify the attributes of an object, perform operations specific to that object,
or interact with other objects.
Here's an example demonstrating the use of an instance method in Python:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height

def area(self):
return self.width * self.height

def perimeter(self):
return 2 * (self.width + self.height)

# Creating an instance of the Rectangle class


rectangle = Rectangle(4, 5)

# Calling instance methods


print("Area:", rectangle.area()) # Output: 20
print("Perimeter:", rectangle.perimeter()) # Output: 18

In this example, area and perimeter are instance methods of the Rectangle class.
Instance methods are defined like regular functions within the class definition,
but they always take at least one parameter, conventionally named self, which
represents the instance on which the method is called.
When calling instance methods, you do not explicitly pass the self parameter;
Python handles this automatically. When you call rectangle.area(), for example,
Python internally calls Rectangle.area(rectangle), where rectangle is the instance
of the Rectangle class.
Inside the instance methods, you can access the instance variables (attributes)
using self. In the area method, for instance, self.width and self.height are used to
access the width and height attributes of the Rectangle object respectively.

Class attributes and destructors


In Python, class attributes are attributes that are associated with the class itself
rather than with instances of the class. They are shared among all instances of the
class. Class attributes are defined outside of any method in the class definition.
Here's an example demonstrating class attributes in Python:

class Car:
# Class attribute
num_cars = 0

def __init__(self, make, model):


self.make = make
self.model = model
Car.num_cars += 1 # Incrementing the class attribute

def __del__(self):
Car.num_cars -= 1 # Decrementing the class attribute when an object
is deleted
# Creating instances of the Car class
car1 = Car("Toyota", "Corolla")
car2 = Car("Honda", "Civic")

# Accessing the class attribute


print("Number of cars:", Car.num_cars) # Output: 2

# Deleting one of the objects


del car1

# After deleting an object, the class attribute will be updated


print("Number of cars:", Car.num_cars) # Output: 1

In this example, num_cars is a class attribute of the Car class. It keeps track of the
total number of car objects created. Every time a new Car object is instantiated,
num_cars is incremented by one.
A destructor in Python is a special method called __del__. It is called when an
object is about to be destroyed (i.e., garbage collected). The purpose of the
destructor is to perform any necessary cleanup actions before the object is
removed from memory. In the example above, when an instance of the Car class
is deleted using the del keyword (del car1), the __del__ method is called, and it
decrements the num_cars class attribute.
Class Method:
Class methods are methods that are called on the class itself, not on a specific
object instance. Therefore, it belongs to a class level, and all class instances share
a class method.

 A class method is bound to the class and not the object of the class. It can
access only class variables.
 It can modify the class state by changing the value of a class variable that
would apply across all the class objects.

In method implementation, if we use only class variables, we should declare such


methods as class methods. The class method has a cls as the first parameter,
which refers to the class.
Class methods are used when we are dealing with factory methods. Factory
methods are those methods that return a class object for different use cases.
The class method can be called using ClassName.method_name () as well as by
using an object of the class.
Example:
class Student:
school_name = 'ABC School'
def __init__(self, name, age):
self.name = name
self.age = age

@classmethod
def change_school(cls, school_name):
# class_name.class_variable
cls.school_name = school_name
# instance method
def show(self):
print(self.name, self.age, 'School:', Student.school_name)

stud1 = Student('Sarthak', 20)


stud1.show()

# change school_name
Student.change_school('XYZ School')
stud1.show()
Any method we create in a class will automatically be created as an instance
method. We must explicitly tell Python that it is a class method using the
@classmethod decorator.
Class methods are defined inside a class, and it is pretty similar to defining a
regular function.
Like, inside an instance method, we use the self keyword to access or modify the
instance variables. Same inside the class method, we use the cls keyword as a
first parameter to access class variables. Therefore the class method gives us
control of changing the class state.
To make a method as class method, add @classmethod decorator before the
method definition, and add cls as the first parameter to the method.
The @classmethod decorator is a built-in function decorator. In Python, we use
the @classmethod decorator to declare a method as a class method.
Static Method:
A static method is a general utility method that performs a task in isolation. Inside
this method, we don’t use instance or class variable because this static method
doesn’t take any parameters like self and cls.
A static method is a general utility method that performs a task in isolation .
A static method is bound to the class and not the object of the class. Therefore,
we can call it using the class name.

A static method doesn’t have access to the class and instance variables because it
does not receive an implicit first argument like self and cls. Therefore it cannot
modify the state of the object or class.

The class method can be called using ClassName.method_name() as well as by


using an object of the class.

Any method we create in a class will automatically be created as an instance


method. We must explicitly tell Python that it is a static method using the
@staticmethod decorator.

Static methods are defined inside a class, and it is pretty similar to defining a
regular function. To declare a static method, use this idiom:

class C:
@staticmethod
def f(arg1, arg2, ...):
...
To make a method a static method, add @staticmethod decorator before the
method definition.

The @staticmethod decorator is a built-in function decorator in Python to declare


a method as a static method.

Example:
class Test :

@staticmethod

def static_method_1():

print('static method 1')

@staticmethod

def static_method_2() :

Test.static_method_1()

@classmethod

def class_method_1(cls) :

cls.static_method_2()

# call class method

Test.class_method_1()
Inheritance in Python:
Inheritance is an important aspect of the object-oriented paradigm. Inheritance
provides code reusability to the program because we can use an existing class to
create a new class instead of creating it from scratch.
In inheritance, the child class acquires the properties and can access all the data
members and functions defined in the parent class. A child class can also provide
its specific implementation to the functions of the parent class.
In python, a derived class can inherit base class by just mentioning the base in the
bracket after the derived class name. Consider the following syntax to inherit a
base class into the derived class.

Syntax: class derived-class(base class):


Pass
Types of Inheritance:

1. Single Inheritance:
Single inheritance in Python refers to the concept where a subclass (child class)
inherits attributes and methods from a single superclass (parent class). This
means that a child class can inherit properties and behaviors from only one
parent class.
Here's a simple example demonstrating single inheritance in Python:
# Parent class (superclass)

class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound")
# Child class (subclass) inheriting from Animal

class Dog(Animal):
def speak(self):
print(f"{self.name} barks")
# Creating instances of both classes

animal = Animal("Generic Animal")


dog = Dog("Tommy")
# Calling methods on instances

animal.speak() # Output: Generic Animal makes a sound

dog.speak() # Output: Buddy barks

In this example:
Animal is the parent class with a constructor method __init__ and a method
speak.
Dog is the child class that inherits from Animal.
Dog overrides the speak method defined in Animal with its own implementation.
Instances of both classes are created, and the speak method is called on each of
them. The output depends on which class the instance belongs to.

Output:
Generic Animal makes a sound
Tommy barks

2. Multilevel Inheritance:
Multilevel inheritance in Python refers to a scenario where a class is derived from
a derived class, i.e., a subclass inherits from another subclass. This creates a
hierarchy of classes with multiple levels of inheritance.
Here's an example demonstrating multilevel inheritance in Python:

# Parent class

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

def speak(self):
print(f"{self.name} makes a sound")

# Child class inheriting from Animal


class Dog(Animal):
def speak(self):
print(f"{self.name} barks")

# Grandchild class inheriting from Dog

class Labrador(Dog):
def color(self):
print(f"{self.name} is brown")

# Creating instances of each class

animal = Animal("Generic Animal")


dog = Dog("Buddy")
labrador = Labrador("Max")

# Calling methods on instances

animal.speak() # Output: Generic Animal makes a sound

dog.speak() # Output: Buddy barks

labrador.speak() # Output: Max barks

labrador.color() # Output: Max is brown

In this example:
Animal is the parent class with a constructor method __init__ and a method
speak.
Dog is a child class of Animal inheriting its attributes and methods. It overrides
the speak method with its own implementation.
Labrador is a grandchild class inheriting from Dog. It inherits both from Animal
and Dog, and it adds its own method color.
Instances of each class are created, and methods are called on them. The output
demonstrates the method calls based on the inheritance hierarchy.

Output:
Generic Animal makes a sound
Buddy barks
Max barks
Max is brown

3. Multiple Inheritance:
Multiple inheritance in Python refers to a situation where a class can inherit
attributes and methods from more than one parent class. This allows for creating
complex class hierarchies by combining features from multiple parent classes.
Here's an example demonstrating multiple inheritance in Python:
# Parent class 1

class Bird:
def __init__(self):
print("Bird is ready")

def who_is_this(self):
print("Bird")

def swim(self):
print("Swim faster")
# Parent class 2

class Mammal:
def __init__(self):
print("Mammal is ready")

def who_is_this(self):
print("Mammal")

def run(self):
print("Run faster")

# Child class inheriting from both Bird and Mammal

class Bat(Bird, Mammal):


def __init__(self):
# Calling constructors of both parent classes

Bird.__init__(self)
Mammal.__init__(self)
print("Bat is ready")

def who_is_this(self):
print("Bat")

# Creating an instance of the Bat class

bat = Bat()
# Calling methods on the instance

bat.who_is_this() # Output: Bat

bat.swim() # Output: Swim faster (from Bird class)

bat.run() # Output: Run faster (from Mammal class)

In this example:

Bird and Mammal are two parent classes, each with its own set of methods.
Bat is a child class inheriting from both Bird and Mammal.
When an instance of Bat is created, both parent class constructors (__init__
methods) are called explicitly to initialize their attributes.
The who_is_this method is overridden in the Bat class to provide its own
implementation.
Methods from both parent classes can be accessed and called on an instance of
the Bat class.
Multiple inheritance can lead to the diamond problem and other complexities, so
it's essential to use it judiciously and understand the method resolution order
(MRO) in Python.

Method Resolution Order (MRO) in Python


Method Resolution Order (MRO) in Python refers to the order in which Python
searches for methods and attributes in a class hierarchy. It's particularly
important in cases of multiple inheritance, where a subclass inherits from more
than one parent class. Understanding the MRO helps Python determine which
method or attribute to use when there are conflicts or ambiguities in the
inheritance hierarchy.
Python uses the C3 linearization algorithm to compute the MRO. This algorithm
ensures that the method resolution order maintains the consistency and
specificity required for resolving method calls and attribute accesses in a
predictable manner.
Here's how you can view the Method Resolution Order in Python:

Let's illustrate MRO with an example:


class A:
def method(self):
print("Method from class A")

class B(A):
def method(self):
print("Method from class B")

class C(A):
def method(self):
print("Method from class C")

class D(B, C):


pass

obj = D()
obj.method()
print(D.__mro__)
Output:
Method from class B
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class
'__main__.A'>, <class 'object'>)

In this example:
We have four classes: A, B, C, and D.
D inherits from both B and C.
B and C both inherit from A.
When we call obj.method(), Python first searches for the method in D, then B,
then C, and finally in A.
The MRO is printed as <class '__main__.D'>, <class '__main__.B'>, <class
'__main__.C'>, <class '__main__.A'>, <class 'object'>.
Understanding the MRO is crucial for correctly resolving method calls and
attribute accesses, especially in cases of multiple inheritance. It helps avoid
ambiguity and ensures that the correct method or attribute is used based on the
class hierarchy.

4. Hierarchical Inheritance:
Hierarchical inheritance in Python refers to a scenario where one parent class has
multiple child classes, but these child classes do not inherit from each other.
Instead, they inherit directly from the same parent class, forming a hierarchical
structure.
Here's an example illustrating hierarchical inheritance in Python:

# Parent class
class Animal:
def __init__(self, name):
self.name = name

def speak(self):
print(f"{self.name} makes a sound")

# Child class 1 inheriting from Animal

class Dog(Animal):
def speak(self):
print(f"{self.name} barks")

# Child class 2 inheriting from Animal

class Cat(Animal):
def speak(self):
print(f"{self.name} meows")

# Creating instances of child classes

dog = Dog("Tommy")
cat = Cat("Mili")

# Calling methods on instances

dog.speak() # Output: Tommy barks

cat.speak() # Output: Mili meows

In this example:
Animal is the parent class with a constructor method __init__ and a
method speak.
Dog and Cat are child classes of Animal, inheriting its attributes and
methods.
Each child class provides its own implementation of the speak method.
Instances of both child classes are created, and the speak method is called
on each of them. The output depends on which child class the instance
belongs to.
Hierarchical inheritance allows for creating a hierarchical structure of
classes where multiple subclasses inherit from a single parent class, but
they do not inherit from each other. Each child class can have its own
unique behavior while sharing common attributes and methods inherited
from the parent class.

5.Hybrid inheritance:
Hybrid inheritance in Python refers to a combination of multiple types of
inheritance, such as single, multiple, and multilevel inheritance, within a single
class hierarchy. It allows for creating complex class relationships by combining
features from different types of inheritance.
Here's an example illustrating hybrid inheritance in Python:

# Parent class 1

class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound")

# Parent class 2

class Flyable:
def fly(self):
print(f"{self.name} can fly")

# Child class inheriting from both Animal and Flyable

class Bird(Animal, Flyable):


# def __init__(self, name):

# super().__init__(name)

pass
# Grandchild class inheriting from Bird

class Parrot(Bird):
def speak(self):
print(f"{self.name} can talk")

# Grandchild class inheriting from Animal

class Dog(Animal):
def speak(self):
print(f"{self.name} barks")

# Creating instances of each class

bird = Bird("Sparrow")
parrot = Parrot("Polly")
dog = Dog("Buddy")

# Calling methods on instances

bird.speak() # Output: Sparrow makes a sound

bird.fly() # Output: Sparrow can fly

parrot.speak() # Output: Polly can talk

parrot.fly() # Output: Polly can fly

dog.speak() # Output: Buddy barks

In this example:
Animal and Flyable are two parent classes, each with its own set of methods.
Bird is a child class inheriting from both Animal and Flyable. This demonstrates
multiple inheritance.
Parrot is a grandchild class inheriting from Bird, showing multilevel inheritance.
Dog is another grandchild class inheriting only from Animal, which demonstrates
single inheritance.
Instances of each class are created, and methods are called on them to showcase
the functionalities inherited from their parent classes.

Super() in Python:
In Python, super() is a built-in function used to access methods and attributes
from the parent class within a subclass. It provides a way to call methods of the
superclass without explicitly naming them, which makes the code more
maintainable and facilitates code reuse.

The super() function returns a proxy object that allows you to invoke methods of
the parent class.
Example:
First, define an Employee class:
class Employee:
def __init__(self, name, base_pay, bonus):
self.name = name
self.base_pay = base_pay
self.bonus = bonus

def get_pay(self):
return self.base_pay + self.bonus

The Employee class has three instance variables name, base_pay, and bonus. It
also has the get_pay() method that returns the total of base_pay and bonus.

Second, define the SalesEmployee class that inherits from the Employee class:
class SalesEmployee(Employee):
def __init__(self, name, base_pay, bonus, sales_incentive):
self.name = name
self.base_pay = base_pay
self.bonus = bonus
self.sales_incentive = sales_incentive

def get_pay(self):
return self.base_pay + self.bonus + self.sales_incentive
The SalesEmployee class has four instance variables name, base_pay, bonus, and
sales_incentive. It has the get_pay() method that overrides the get_pay() method
in the Employee class.

super().__init__()
The __init__() method of the SalesEmployee class has some parts that are the
same as the ones in the __init__() method of the Employee class.
To avoid duplication, you can call the __init__() method of Employee class from
the __init__() method of the SalesEmployee class.
To reference the Employee class in the SalesEmployee class, you use the super().
The super() returns a reference of the parent class from a child class.
The following redefines the SalesEmployee class that uses the super() to call the
__init__() method of the Employee class:

class SalesEmployee(Employee):
def __init__(self, name, base_pay, bonus, sales_incentive):
super().__init__(name, base_pay, bonus)
self.sales_incentive = sales_incentive

def get_pay(self):
return self.base_pay + self.bonus + self.sales_incentive

When you create an instance of the SalesEmployee class, Python will execute the
__init__() method in the SalesEmployee class. In turn, this __init__() method calls
the __init__() method of the Employee class to initialize the name, base_pay,
and bonus.
Delegating to other methods in the parent class
The get_pay() method of the SalesEmployee class has some logic that is already
defined in the get_pay() method of the Employee class. Therefore, you can reuse
this logic in the get_pay() method of the SalesEmployee class.

To do that, you can call the get_pay() method of the Employee class in the
get_pay() method of SalesEmployee class as follows:

class SalesEmployee(Employee):
def __init__(self, name, base_pay, bonus, sales_incentive):
super().__init__(name, base_pay, bonus)
self.sales_incentive = sales_incentive

def get_pay(self):
return super().get_pay() + self.sales_incentive

The following calls the get_pay() method of the Employee class from the
get_pay() method in the SalesEmployee class:

super().get_pay()
When you call the get_pay() method from an instance of the SalesEmployee
class, it calls the get_pay() method from the parent class (Employee) and return
the sum of the result of the super().get_pay() method with the sales_incentive.

Complete Program:
class Employee:
def __init__(self, name, base_pay, bonus):
self.name = name
self.base_pay = base_pay
self.bonus = bonus

def get_pay(self):
return self.base_pay + self.bonus

class SalesEmployee(Employee):
def __init__(self, name, base_pay, bonus, sales_incentive):
super().__init__(name, base_pay, bonus)
self.sales_incentive = sales_incentive

def get_pay(self):
return super().get_pay() + self.sales_incentive

if __name__ == '__main__':
sales_employee = SalesEmployee('John', 5000, 1000, 2000)
print(sales_employee.get_pay()) # 8000

 Deligation in python:
Delegation in Python refers to the practice of passing the responsibility for a
particular task to another object. It is a design pattern where an object forwards
method calls and attribute access to another object, which is responsible for
handling them. Delegation is commonly used to achieve code reuse and
modularization by composing objects rather than inheriting behavior from a
superclass.
Here's a simple example to illustrate delegation in Python:
class Printer:
def print_document(self, document):
print(f"Printing document: {document}")

class Scanner:
def scan_document(self, document):
print(f"Scanning document: {document}")

class Copier:
def __init__(self):
self.printer = Printer()
self.scanner = Scanner()

def copy_document(self, document):


self.scanner.scan_document(document)
self.printer.print_document(document)

copier = Copier()
copier.copy_document("Sample Document")
In this example:
We have three classes: Printer, Scanner, and Copier.
Printer and Scanner represent objects responsible for printing and scanning
documents, respectively.
Copier is a class that delegates printing and scanning tasks to instances of Printer
and Scanner.
In the Copier class, __init__ method initializes instances of Printer and Scanner.
The copy_document method in the Copier class delegates the scanning and
printing tasks to the Scanner and Printer objects, respectively.

You might also like