Python Programming for Beginners

Chapter 16 – Python Object-Oriented Programming: Classes and Objects

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