8. Chapter 8: Python Object & Class

8.1. Python Objects and Classes

8.1.1. 1. What is a Class and an Object

A class is a blueprint for creating objects. An object is an instance of a class:

class Person:
    pass

p1 = Person()
print(p1)

Classes define structure; objects represent real entities.

8.1.2. 2. Defining a Class with Attributes

Attributes represent properties of the class:

class Person:
    name = "Alice"
    age = 25

p = Person()
print(p.name)
print(p.age)

8.1.3. 3. Constructor Method (__init__)

The constructor initializes object data at creation:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person("Bob", 30)
print(p.name, p.age)

8.1.4. 4. Instance Methods

Instance methods operate on object data using self:

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

    def greet(self):
        return f"Hello, my name is {self.name}"

p = Person("Alice")
print(p.greet())

8.1.5. 5. Class Variables vs Instance Variables

Class variables are shared, instance variables are unique per object:

class Student:
    school = "Global Academy"  # Class variable

    def __init__(self, name):
        self.name = name        # Instance variable

s1 = Student("Alice")
s2 = Student("Bob")

print(s1.school)
print(s2.school)

8.1.6. 6. Creating Multiple Objects

Multiple objects can be created from the same class:

class Car:
    def __init__(self, brand):
        self.brand = brand

c1 = Car("Toyota")
c2 = Car("Tesla")

print(c1.brand)
print(c2.brand)

8.1.7. 7. Modifying Object Properties

Objects allow runtime modification of attributes:

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

emp = Employee("John")
emp.name = "Michael"

print(emp.name)

8.1.8. 8. Deleting Object Properties and Objects

Use del to remove attributes or the object itself:

class Product:
    def __init__(self, price):
        self.price = price

item = Product(100)

del item.price
# del item  # Deletes the object entirely

8.1.9. 9. Built-in Object Functions

Common object-related built-ins:

  • type()

  • isinstance()

class User:
    pass

user = User()

print(isinstance(user, User))  # True
print(type(user))              # <class '__main__.User'>

8.1.10. 10. Real-World Class Example

Encapsulation of data and behavior into a single unit:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.balance)  # Output: 1500

8.2. Python Inheritance

8.2.1. 1. What is Inheritance

Inheritance allows a class (child) to acquire properties and behaviors of another class (parent):

class Animal:
    def eat(self):
        print("Animal is eating")

class Dog(Animal):
    pass

d = Dog()
d.eat()  # Inherited method

Promotes code reusability and hierarchical design.

8.2.2. 2. Single Inheritance

A child class inherits from one parent class:

class Parent:
    def show(self):
        print("This is the parent class")

class Child(Parent):
    pass

c = Child()
c.show()

8.2.3. 3. Adding New Methods in Child Class

Child classes can extend functionality beyond the parent:

class Animal:
    def sound(self):
        print("Some sound")

class Dog(Animal):
    def bark(self):
        print("Dog barks")

d = Dog()
d.sound()
d.bark()

8.2.4. 4. Method Overriding

Child class can redefine parent methods:

class Animal:
    def sound(self):
        print("Animal makes sound")

class Dog(Animal):
    def sound(self):
        print("Dog barks")

d = Dog()
d.sound()  # Dog barks

8.2.5. 5. Using super() to Call Parent Methods

super() invokes the parent class method within the child:

class Animal:
    def sound(self):
        print("Animal makes sound")

class Dog(Animal):
    def sound(self):
        super().sound()
        print("Dog barks")

d = Dog()
d.sound()

8.2.6. 6. Multilevel Inheritance

Inheritance chain stretches across multiple levels:

class Grandparent:
    def feature(self):
        print("Grandparent feature")

class Parent(Grandparent):
    pass

class Child(Parent):
    pass

c = Child()
c.feature()

8.2.7. 7. Multiple Inheritance

A child class inherits from multiple parent classes:

class Father:
    def skill(self):
        print("Driving")

class Mother:
    def talent(self):
        print("Cooking")

class Child(Father, Mother):
    pass

c = Child()
c.skill()
c.talent()

8.2.8. 8. Method Resolution Order (MRO)

MRO defines the order in which methods are resolved in inheritance:

class A:
    def show(self):
        print("A")

class B(A):
    def show(self):
        print("B")

class C(B):
    pass

c = C()
c.show()
print(C.mro())

8.2.9. 9. Constructor Inheritance

Child constructors can call parent constructors using super():

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

class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

c = Child("Alice", 25)
print(c.name, c.age)

8.2.10. 10. Real-World Inheritance Example

Demonstrates inheritance in an object-oriented system:

class Vehicle:
    def start(self):
        print("Vehicle started")

class Car(Vehicle):
    def drive(self):
        print("Car is driving")

class Bike(Vehicle):
    def ride(self):
        print("Bike is riding")

car = Car()
bike = Bike()

car.start()
car.drive()

bike.start()
bike.ride()

8.3. Python Multiple Inheritance

8.3.1. 1. What is Multiple Inheritance

Multiple inheritance occurs when a class inherits from more than one parent class:

class Father:
    def skill1(self):
        print("Driving")

class Mother:
    def skill2(self):
        print("Cooking")

class Child(Father, Mother):
    pass

c = Child()
c.skill1()
c.skill2()

The child class gains features from all parent classes.

8.3.2. 2. Basic Multiple Inheritance Structure

The child class can access methods from both parents:

class A:
    def show_a(self):
        print("Class A")

class B:
    def show_b(self):
        print("Class B")

class C(A, B):
    pass

obj = C()
obj.show_a()
obj.show_b()

8.3.3. 3. Overlapping Method Names

Python follows Method Resolution Order (MRO) to determine which method to invoke:

class A:
    def display(self):
        print("From A")

class B:
    def display(self):
        print("From B")

class C(A, B):
    pass

obj = C()
obj.display()

8.3.4. 4. Understanding Method Resolution Order (MRO)

MRO defines the order of class traversal: C → A → B → object

class A:
    def show(self):
        print("A")

class B:
    def show(self):
        print("B")

class C(A, B):
    pass

print(C.mro())

8.3.5. 5. Calling Parent Methods Explicitly

Allows execution of logic from multiple parents:

class A:
    def process(self):
        print("Process A")

class B:
    def process(self):
        print("Process B")

class C(A, B):
    def process(self):
        A.process(self)
        B.process(self)

c = C()
c.process()

8.3.6. 6. Multiple Inheritance with Constructors

Only the first parent’s constructor executes due to MRO:

class A:
    def __init__(self):
        print("Constructor A")

class B:
    def __init__(self):
        print("Constructor B")

class C(A, B):
    def __init__(self):
        super().__init__()

c = C()

8.3.7. 7. Cooperative Multiple Inheritance Using super()

Demonstrates cooperative method chaining through MRO:

class A:
    def __init__(self):
        print("A init")
        super().__init__()

class B:
    def __init__(self):
        print("B init")
        super().__init__()

class C(A, B):
    def __init__(self):
        print("C init")
        super().__init__()

c = C()

8.3.8. 8. Diamond Problem Example

Python resolves this ambiguity using MRO, avoiding duplicate calls:

class Grandparent:
    def greet(self):
        print("Hello from Grandparent")

class Parent1(Grandparent):
    pass

class Parent2(Grandparent):
    pass

class Child(Parent1, Parent2):
    pass

c = Child()
c.greet()

8.3.9. 9. Visualizing the Inheritance Hierarchy

Helps understand order of class resolution for debugging:

class X:
    pass

class Y(X):
    pass

class Z(X):
    pass

class W(Y, Z):
    pass

print(W.mro())

8.3.10. 10. Real-World Multiple Inheritance Example

Combines cross-cutting concerns such as logging and validation:

class Logger:
    def log(self):
        print("Logging data")

class Validator:
    def validate(self):
        print("Validating data")

class Service(Logger, Validator):
    def execute(self):
        self.log()
        self.validate()
        print("Executing service")

service = Service()
service.execute()

8.4. Polymorphism in Python

8.4.1. 1. What is Polymorphism

Polymorphism allows the same interface (method or operator) to behave differently based on the object or context:

print(len("Python"))   # String length
print(len([1, 2, 3]))  # List length

The same function works for multiple data types.

8.4.2. 2. Function Polymorphism

A single function adapts behavior based on arguments:

def add(a, b, c=0):
    return a + b + c

print(add(2, 3))        # 5
print(add(2, 3, 4))     # 9

8.4.3. 3. Built-in Polymorphism

The + operator behaves differently based on operand types:

print(5 + 10)          # Integer addition
print("Hello " + "AI") # String concatenation

8.4.4. 4. Method Overriding (Runtime Polymorphism)

Child class alters the behavior of a parent method:

class Animal:
    def speak(self):
        print("Animal sound")

class Dog(Animal):
    def speak(self):
        print("Dog barks")

pet = Dog()
pet.speak()

8.4.5. 5. Operator Overloading

Custom behavior for operators using magic methods:

class Vector:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        return Vector(self.x + other.x)

v1 = Vector(10)
v2 = Vector(20)
v3 = v1 + v2

print(v3.x)  # Output: 30

8.4.6. 6. Polymorphism with Different Classes

Different objects responding to the same method call:

class Cat:
    def sound(self):
        print("Meow")

class Dog:
    def sound(self):
        print("Bark")

def make_sound(animal):
    animal.sound()

make_sound(Cat())
make_sound(Dog())

8.4.7. 7. Duck Typing

Behavior is determined by method presence, not class type:

class Car:
    def move(self):
        print("Car moving")

class Boat:
    def move(self):
        print("Boat sailing")

def start(vehicle):
    vehicle.move()

start(Car())
start(Boat())

8.4.8. 8. Method Overloading via Default Arguments

Python simulates overloading using default parameters:

class Calculator:
    def multiply(self, a, b=1):
        return a * b

calc = Calculator()
print(calc.multiply(5))     # 5
print(calc.multiply(5, 3))  # 15

8.4.9. 9. Abstract Base Class Polymorphism

Ensures consistent interface across implementations:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def area(self):
        return 10 * 5

shape = Rectangle()
print(shape.area())

8.4.10. 10. Real-World Polymorphism Example

Multiple behaviors implemented under a unified interface:

class Notification:
    def send(self):
        print("Sending notification")

class Email(Notification):
    def send(self):
        print("Sending Email")

class SMS(Notification):
    def send(self):
        print("Sending SMS")

def notify(service):
    service.send()

notify(Email())
notify(SMS())

8.5. Python Operator Overloading

8.5.1. 1. What is Operator Overloading

Operator overloading allows custom classes to define how standard operators behave using special (dunder) methods:

class Number:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        return self.value + other.value

n1 = Number(10)
n2 = Number(20)

print(n1 + n2)  # Output: 30

The + operator is redefined for the Number class.

8.5.2. 2. Overloading the Addition Operator (+)

Implements vector addition logic:

class Vector:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        return Vector(self.x + other.x)

v1 = Vector(5)
v2 = Vector(7)
v3 = v1 + v2

print(v3.x)  # Output: 12

8.5.3. 3. Overloading the Subtraction Operator (-)

Custom logic for subtraction:

class Balance:
    def __init__(self, amount):
        self.amount = amount

    def __sub__(self, other):
        return Balance(self.amount - other.amount)

b1 = Balance(100)
b2 = Balance(40)

result = b1 - b2
print(result.amount)  # Output: 60

8.5.4. 4. Overloading Multiplication Operator (*)

Defines how objects behave with *:

class Repeater:
    def __init__(self, text):
        self.text = text

    def __mul__(self, times):
        return self.text * times

r = Repeater("AI ")
print(r * 3)  # Output: AI AI AI

8.5.5. 5. Overloading Division Operator (/)

Implements custom division logic:

class Calculator:
    def __init__(self, value):
        self.value = value

    def __truediv__(self, other):
        return self.value / other.value

c1 = Calculator(100)
c2 = Calculator(4)

print(c1 / c2)  # Output: 25.0

8.5.6. 6. Overloading Equality Operator (==)

Controls object comparison behavior:

class Point:
    def __init__(self, x):
        self.x = x

    def __eq__(self, other):
        return self.x == other.x

p1 = Point(5)
p2 = Point(5)

print(p1 == p2)  # True

8.5.7. 7. Overloading Less Than and Greater Than

Supports sorting and ranking operations:

class Score:
    def __init__(self, marks):
        self.marks = marks

    def __lt__(self, other):
        return self.marks < other.marks

    def __gt__(self, other):
        return self.marks > other.marks

s1 = Score(85)
s2 = Score(70)

print(s1 > s2)  # True

8.5.8. 8. Overloading In-place Operators (+=)

Modifies object in-place:

class Counter:
    def __init__(self, value):
        self.value = value

    def __iadd__(self, other):
        self.value += other
        return self

c = Counter(10)
c += 5
print(c.value)  # Output: 15

8.5.9. 9. Overloading String Representation (str())

Enhances readability of object display:

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __str__(self):
        return f"{self.name} costs ${self.price}"

p = Product("Laptop", 1200)
print(p)  # Laptop costs $1200

8.5.10. 10. Comprehensive Operator Overloading Example

Fully custom arithmetic for a domain-specific class:

class ComplexNumber:
    def __init__(self, real):
        self.real = real

    def __add__(self, other):
        return ComplexNumber(self.real + other.real)

    def __sub__(self, other):
        return ComplexNumber(self.real - other.real)

    def __str__(self):
        return str(self.real)

c1 = ComplexNumber(10)
c2 = ComplexNumber(3)

print(c1 + c2)  # 13
print(c1 - c2)  # 7