<h2>16.1 Introduction to Object-Oriented Programming</h2>
<p>
Object-Oriented Programming, commonly called OOP, is a programming
approach in which programs are organized around objects and classes.
</p>
<p>
Objects can contain data and functions that work with that data.
Python supports object-oriented programming and allows you to create
your own classes and objects.
</p>
<h2>16.2 What is a Class?</h2>
<p>
A class is a blueprint or template used to create objects. It can
define the data and behavior that objects created from the class
will have.
</p>
<pre><code>class Student:
pass</code></pre>
<p>
Here, <code>Student</code> is a class. The <code>pass</code> statement
means that the class currently has no implementation.
</p>
<h2>16.3 What is an Object?</h2>
<p>
An object is an instance of a class. After creating a class, you can
create multiple objects from it.
</p>
<pre><code>class Student:
pass
student1 = Student()
student2 = Student()
print(student1)
print(student2)</code></pre>
<h2>16.4 Creating a Simple Class</h2>
<pre><code>class Car:
brand = "Toyota"
car1 = Car()
print(car1.brand)</code></pre>
<p>Output:</p>
<pre><code>Toyota</code></pre>
<h2>16.5 Class Attributes</h2>
<p>
A variable defined inside a class but outside its methods is commonly
called a class attribute.
</p>
<pre><code>class Student:
school = "ABC School"
student1 = Student()
print(student1.school)</code></pre>
<h2>16.6 Creating the __init__() Method</h2>
<p>
The <code>__init__()</code> method is commonly used to initialize an
object when it is created.
</p>
<pre><code>class Student:
def __init__(self):
print("Student object created")
student1 = Student()</code></pre>
<p>Output:</p>
<pre><code>Student object created</code></pre>
<h2>16.7 The self Parameter</h2>
<p>
The <code>self</code> parameter refers to the current object. It is
used inside instance methods to access data and methods belonging to
that object.
</p>
<pre><code>class Student:
def __init__(self, name):
self.name = name
student1 = Student("Aman")
print(student1.name)</code></pre>
<p>Output:</p>
<pre><code>Aman</code></pre>
<h2>16.8 Creating Multiple Objects</h2>
<p>
A single class can be used to create many objects with different
data.
</p>
<pre><code>class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
student1 = Student("Aman", 85)
student2 = Student("Ravi", 92)
print(student1.name)
print(student1.marks)
print(student2.name)
print(student2.marks)</code></pre>
<h2>16.9 Instance Attributes</h2>
<p>
Attributes stored separately for each object are called instance
attributes.
</p>
<pre><code>class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
employee1 = Employee("Aman", 30000)
employee2 = Employee("Ravi", 40000)
print(employee1.name)
print(employee2.name)</code></pre>
<h2>16.10 Creating Methods</h2>
<p>
A function defined inside a class is called a method.
</p>
<pre><code>class Student:
def __init__(self, name):
self.name = name
def greet(self):
print("Hello", self.name)
student1 = Student("Aman")
student1.greet()</code></pre>
<p>Output:</p>
<pre><code>Hello Aman</code></pre>
<h2>16.11 Methods with Parameters</h2>
<pre><code>class Calculator:
def add(self, a, b):
return a + b
calculator = Calculator()
print(calculator.add(10, 20))</code></pre>
<p>Output:</p>
<pre><code>30</code></pre>
<h2>16.12 Creating a Student Class</h2>
<pre><code>class Student:
def __init__(self, name, age, marks):
self.name = name
self.age = age
self.marks = marks
def display(self):
print("Name:", self.name)
print("Age:", self.age)
print("Marks:", self.marks)
student = Student("Aman", 16, 88)
student.display()</code></pre>
<h2>16.13 Updating Object Attributes</h2>
<p>
Object attributes can be changed after the object has been created.
</p>
<pre><code>class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
student = Student("Aman", 80)
print(student.marks)
student.marks = 95
print(student.marks)</code></pre>
<p>Output:</p>
<pre><code>80
95</code></pre>
<h2>16.14 Adding New Attributes</h2>
<p>
Python also allows attributes to be added to an individual object.
</p>
<pre><code>class Student:
pass
student = Student()
student.name = "Aman"
student.marks = 90
print(student.name)
print(student.marks)</code></pre>
<h2>16.15 Deleting an Attribute</h2>
<p>
The <code>del</code> statement can be used to remove an object
attribute.
</p>
<pre><code>class Student:
def __init__(self, name):
self.name = name
student = Student("Aman")
print(student.name)
del student.name</code></pre>
<h2>16.16 Deleting an Object</h2>
<p>
The <code>del</code> statement can also remove a reference to an object.
</p>
<pre><code>class Student:
pass
student = Student()
del student</code></pre>
<h2>16.17 Class Attributes vs Instance Attributes</h2>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Class Attribute</th>
<th>Instance Attribute</th>
</tr>
</thead>
<tbody>
<tr>
<td>Belongs to</td>
<td>Class</td>
<td>Individual object</td>
</tr>
<tr>
<td>Shared</td>
<td>Usually shared by instances</td>
<td>Usually specific to each instance</td>
</tr>
<tr>
<td>Example</td>
<td><code>school</code></td>
<td><code>name</code></td>
</tr>
</tbody>
</table>
<h2>16.18 Class Methods</h2>
<p>
A class method works with the class rather than a particular instance.
It is created using the <code>@classmethod</code> decorator and
usually receives <code>cls</code> as its first parameter.
</p>
<pre><code>class Student:
school = "ABC School"
@classmethod
def show_school(cls):
print(cls.school)
Student.show_school()</code></pre>
<h2>16.19 Static Methods</h2>
<p>
A static method is a method that does not require the instance or class
as its first parameter. It can be created using
<code>@staticmethod</code>.
</p>
<pre><code>class Calculator:
@staticmethod
def add(a, b):
return a + b
print(Calculator.add(10, 20))</code></pre>
<h2>16.20 Instance Methods</h2>
<p>
Instance methods operate on a particular object and normally receive
<code>self</code> as their first parameter.
</p>
<pre><code>class Student:
def __init__(self, name):
self.name = name
def display_name(self):
print(self.name)
student = Student("Aman")
student.display_name()</code></pre>
<h2>16.21 Constructor and Destructor</h2>
<p>
The <code>__init__()</code> method is commonly used to initialize an
object. Python also provides <code>__del__()</code>, which can be
defined to perform cleanup when an object is being destroyed.
</p>
<pre><code>class Student:
def __init__(self, name):
self.name = name
print("Object created")
def __del__(self):
print("Object cleanup")
student = Student("Aman")</code></pre>
<p>
The exact timing of object cleanup should not be relied upon for
important resource management. Context managers are generally better
for managing resources such as files.
</p>
<h2>16.22 Encapsulation</h2>
<p>
Encapsulation means keeping related data and methods together inside
a class and controlling how the internal data is accessed.
</p>
<pre><code>class BankAccount:
def __init__(self, balance):
self.balance = balance
def deposit(self, amount):
self.balance += amount
def show_balance(self):
print("Balance:", self.balance)
account = BankAccount(1000)
account.deposit(500)
account.show_balance()</code></pre>
<h2>16.23 Protected Naming Convention</h2>
<p>
A single leading underscore is commonly used as a convention to
indicate that an attribute is intended for internal use.
</p>
<pre><code>class Student:
def __init__(self, name):
self._name = name
student = Student("Aman")
print(student._name)</code></pre>
<p>
The underscore is a convention and does not by itself make the
attribute inaccessible.
</p>
<h2>16.24 Private Naming Convention and Name Mangling</h2>
<p>
Names beginning with two underscores are subject to Python's
name-mangling mechanism. This can help avoid accidental name conflicts
in subclasses.
</p>
<pre><code>class Student:
def __init__(self):
self.__marks = 90
student = Student()
print(student._Student__marks)</code></pre>
<h2>16.25 Getter Method</h2>
<p>
A getter method can be used to retrieve an internal value through a
class method.
</p>
<pre><code>class Student:
def __init__(self, marks):
self.__marks = marks
def get_marks(self):
return self.__marks
student = Student(90)
print(student.get_marks())</code></pre>
<h2>16.26 Setter Method</h2>
<p>
A setter method can be used to update an internal value after applying
appropriate validation.
</p>
<pre><code>class Student:
def __init__(self, marks):
self.__marks = marks
def set_marks(self, marks):
if 0 <= marks <= 100:
self.__marks = marks
def get_marks(self):
return self.__marks
student = Student(80)
student.set_marks(95)
print(student.get_marks())</code></pre>
<h2>16.27 Property Decorator</h2>
<p>
The <code>@property</code> decorator can provide a convenient interface
for accessing a method like an attribute.
</p>
<pre><code>class Student:
def __init__(self, marks):
self._marks = marks
@property
def marks(self):
return self._marks
student = Student(90)
print(student.marks)</code></pre>
<h2>16.28 Property Setter</h2>
<pre><code>class Student:
def __init__(self, marks):
self._marks = marks
@property
def marks(self):
return self._marks
@marks.setter
def marks(self, value):
if 0 <= value <= 100:
self._marks = value
else:
raise ValueError("Marks must be between 0 and 100.")
student = Student(80)
student.marks = 95
print(student.marks)</code></pre>
<h2>16.29 Inheritance</h2>
<p>
Inheritance allows one class to reuse and extend the functionality of
another class.
</p>
<pre><code>class Animal:
def speak(self):
print("Animal makes a sound")
class Dog(Animal):
pass
dog = Dog()
dog.speak()</code></pre>
<h2>16.30 Parent and Child Classes</h2>
<p>
The class being inherited from is often called the parent or base class.
The class that inherits from it is called the child or derived class.
</p>
<pre><code>class Vehicle:
def start(self):
print("Vehicle started")
class Car(Vehicle):
def drive(self):
print("Car is moving")
car = Car()
car.start()
car.drive()</code></pre>
<h2>16.31 Adding a Constructor in a Child Class</h2>
<pre><code>class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
dog = Dog("Bruno", "Labrador")
print(dog.name)
print(dog.breed)</code></pre>
<h2>16.32 The super() Function</h2>
<p>
The <code>super()</code> function can be used to call functionality
from a parent class.
</p>
<pre><code>class Person:
def __init__(self, name):
self.name = name
class Student(Person):
def __init__(self, name, marks):
super().__init__(name)
self.marks = marks
student = Student("Aman", 90)
print(student.name)
print(student.marks)</code></pre>
<h2>16.33 Method Overriding</h2>
<p>
A child class can provide its own implementation of a method that
exists in the parent class. This is called method overriding.
</p>
<pre><code>class Animal:
def sound(self):
print("Some animal sound")
class Dog(Animal):
def sound(self):
print("Dog barks")
animal = Animal()
dog = Dog()
animal.sound()
dog.sound()</code></pre>
<h2>16.34 Polymorphism</h2>
<p>
Polymorphism means that the same method name or interface can work
with objects of different classes.
</p>
<pre><code>class Dog:
def sound(self):
print("Bark")
class Cat:
def sound(self):
print("Meow")
animals = [Dog(), Cat()]
for animal in animals:
animal.sound()</code></pre>
<h2>16.35 Duck Typing</h2>
<p>
Python often focuses on whether an object supports the required
operation rather than requiring it to belong to a particular class.
This idea is commonly described as duck typing.
</p>
<pre><code>class Dog:
def speak(self):
print("Woof")
class Person:
def speak(self):
print("Hello")
def make_speak(obj):
obj.speak()
make_speak(Dog())
make_speak(Person())</code></pre>
<h2>16.36 Multiple Inheritance</h2>
<p>
Python allows a class to inherit from more than one parent class.
</p>
<pre><code>class Father:
def father_feature(self):
print("Father feature")
class Mother:
def mother_feature(self):
print("Mother feature")
class Child(Father, Mother):
pass
child = Child()
child.father_feature()
child.mother_feature()</code></pre>
<h2>16.37 Multilevel Inheritance</h2>
<p>
In multilevel inheritance, a class inherits from another child class,
creating multiple levels of inheritance.
</p>
<pre><code>class Grandparent:
def show_grandparent(self):
print("Grandparent")
class Parent(Grandparent):
def show_parent(self):
print("Parent")
class Child(Parent):
def show_child(self):
print("Child")
child = Child()
child.show_grandparent()
child.show_parent()
child.show_child()</code></pre>
<h2>16.38 Hierarchical Inheritance</h2>
<p>
In hierarchical inheritance, multiple child classes inherit from the
same parent class.
</p>
<pre><code>class Animal:
def eat(self):
print("Animal eats")
class Dog(Animal):
pass
class Cat(Animal):
pass
dog = Dog()
cat = Cat()
dog.eat()
cat.eat()</code></pre>
<h2>16.39 Abstract Classes</h2>
<p>
Abstract classes can define methods that subclasses are expected to
implement. Python provides the <code>abc</code> module for creating
abstract base classes.
</p>
<pre><code>from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
square = Square(5)
print(square.area())</code></pre>
<h2>16.40 Special Methods</h2>
<p>
Python classes can define special methods, sometimes called
dunder methods, using names that begin and end with double underscores.
</p>
<pre><code>class Student:
def __init__(self, name):
self.name = name
student = Student("Aman")
print(student.name)</code></pre>
<h2>16.41 The __str__() Method</h2>
<p>
The <code>__str__()</code> method defines a human-readable string
representation of an object.
</p>
<pre><code>class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def __str__(self):
return f"{self.name} - {self.marks}"
student = Student("Aman", 90)
print(student)</code></pre>
<p>Output:</p>
<pre><code>Aman - 90</code></pre>
<h2>16.42 The __len__() Method</h2>
<p>
The <code>__len__()</code> method can define what the
<code>len()</code> function returns for an object.
</p>
<pre><code>class Classroom:
def __init__(self, students):
self.students = students
def __len__(self):
return len(self.students)
classroom = Classroom(["Aman", "Ravi", "Neha"])
print(len(classroom))</code></pre>
<p>Output:</p>
<pre><code>3</code></pre>
<h2>16.43 Comparing Objects</h2>
<p>
Special methods such as <code>__eq__()</code> can be used to define
how objects should be compared.
</p>
<pre><code>class Student:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
student1 = Student("Aman")
student2 = Student("Aman")
print(student1 == student2)</code></pre>
<p>Output:</p>
<pre><code>True</code></pre>
<h2>16.44 Composition</h2>
<p>
Composition is a design approach where an object contains another
object and uses it as part of its functionality.
</p>
<pre><code>class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()
print("Car started")
car = Car()
car.start()</code></pre>
<h2>16.45 Class Design Example</h2>
<pre><code>class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount > 0:
self.balance += amount
def withdraw(self, amount):
if 0 < amount <= self.balance:
self.balance -= amount
return True
return False
def show_balance(self):
print("Owner:", self.owner)
print("Balance:", self.balance)
account = BankAccount("Aman", 5000)
account.deposit(1000)
account.withdraw(2000)
account.show_balance()</code></pre>
<h2>16.46 Student Management Example</h2>
<pre><code>class Student:
def __init__(self, name, roll_no, marks):
self.name = name
self.roll_no = roll_no
self.marks = marks
def display(self):
print("Name:", self.name)
print("Roll No:", self.roll_no)
print("Marks:", self.marks)
def result(self):
if self.marks >= 40:
return "Pass"
return "Fail"
student = Student("Aman", 101, 78)
student.display()
print("Result:", student.result())</code></pre>
<h2>16.47 Employee Management Example</h2>
<pre><code>class Employee:
def __init__(self, name, department, salary):
self.name = name
self.department = department
self.salary = salary
def display(self):
print("Name:", self.name)
print("Department:", self.department)
print("Salary:", self.salary)
employee = Employee(
"Ravi",
"Computer Science",
45000
)
employee.display()</code></pre>
<h2>16.48 Advantages of OOP</h2>
<ul>
<li>Organizes complex programs into classes and objects.</li>
<li>Encourages code reuse.</li>
<li>Makes programs easier to maintain.</li>
<li>Supports inheritance and polymorphism.</li>
<li>Helps keep related data and behavior together.</li>
<li>Can make large applications easier to extend.</li>
</ul>
<h2>16.49 Important OOP Concepts</h2>
<table>
<thead>
<tr>
<th>Concept</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>Class</td>
<td>Blueprint used to create objects</td>
</tr>
<tr>
<td>Object</td>
<td>Instance of a class</td>
</tr>
<tr>
<td>Encapsulation</td>
<td>Combining data and behavior and controlling access</td>
</tr>
<tr>
<td>Inheritance</td>
<td>Reusing and extending another class</td>
</tr>
<tr>
<td>Polymorphism</td>
<td>Using a common interface with different object types</td>
</tr>
<tr>
<td>Abstraction</td>
<td>Defining essential interfaces while hiding implementation details</td>
</tr>
</tbody>
</table>
<h2>16.50 Chapter Summary</h2>
<p>
In this chapter, you learned the fundamentals of Object-Oriented
Programming in Python. You learned how to create classes and objects,
define attributes and methods, use constructors, and build reusable
program structures.
</p>
<ul>
<li>Classes and objects</li>
<li>The <code>__init__()</code> method</li>
<li>The <code>self</code> parameter</li>
<li>Instance and class attributes</li>
<li>Instance, class, and static methods</li>
<li>Encapsulation</li>
<li>Properties and setters</li>
<li>Inheritance</li>
<li>The <code>super()</code> function</li>
<li>Method overriding</li>
<li>Polymorphism</li>
<li>Multiple and multilevel inheritance</li>
<li>Abstract classes</li>
<li>Special methods</li>
<li>Composition</li>
</ul>
<h2>16.51 Quick Revision Questions</h2>
<ol>
<li>What is a class?</li>
<li>What is an object?</li>
<li>What is the purpose of <code>__init__()</code>?</li>
<li>What does <code>self</code> represent?</li>
<li>What is an instance attribute?</li>
<li>What is inheritance?</li>
<li>What is method overriding?</li>
<li>What is polymorphism?</li>
<li>What is the purpose of <code>super()</code>?</li>
<li>What is encapsulation?</li>
</ol>
<h2>16.52 Practice Exercises</h2>
<ol>
<li>Create a Student class with name, age, and marks.</li>
<li>Create a method that displays student information.</li>
<li>Create an Employee class with name and salary.</li>
<li>Create a BankAccount class with deposit and withdrawal methods.</li>
<li>Create a Car class with start and stop methods.</li>
<li>Create a parent Animal class and child Dog class.</li>
<li>Demonstrate method overriding using two classes.</li>
<li>Create a class containing a private attribute and getter method.</li>
<li>Create a class method and a static method.</li>
<li>Create a small program demonstrating polymorphism.</li>
</ol>
<h2>16.53 Mini Project: Student Management System</h2>
<p>
Create a simple student management program using classes and objects.
The program should store student information and provide methods to
display the information and determine the result.
</p>
<pre><code>class Student:
def __init__(self, name, roll_no, marks):
self.name = name
self.roll_no = roll_no
self.marks = marks
def display(self):
print("Name:", self.name)
print("Roll No:", self.roll_no)
print("Marks:", self.marks)
def result(self):
if self.marks >= 40:
return "Pass"
return "Fail"
student1 = Student("Aman", 101, 85)
student2 = Student("Ravi", 102, 32)
student1.display()
print("Result:", student1.result())
print()
student2.display()
print("Result:", student2.result())</code></pre>
<h2>What's Next?</h2>
<p>
<strong>
Next Chapter: Python Inheritance and Polymorphism – Reusing and Extending Classes
</strong>
</p>