Latest update Android YouTube

Advanced OOP | OOP in Python

Advanced Object-Oriented Programming in Python

Mastering inheritance, polymorphism, encapsulation, and Python's advanced OOP features

1. Inheritance

Basic Inheritance

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        raise NotImplementedError("Subclass must implement")

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Usage
animals = [Dog("Rex"), Cat("Whiskers")]
for animal in animals:
    print(animal.speak())

The super() Function

Access parent class methods and properties:

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

class Child(Parent):
    def __init__(self, value, extra):
        super().__init__(value)  # Call parent __init__
        self.extra = extra

Type Checking

dog = Dog("Buddy")
print(isinstance(dog, Dog))    # True
print(isinstance(dog, Animal)) # True
print(issubclass(Dog, Animal)) # True

Types of Inheritance

Single Inheritance

Parent
|
Child

Multiple Inheritance

Parent1 Parent2
\ /
Child
Method Resolution Order (MRO)
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, 
#  <class '__main__.C'>, <class '__main__.A'>, 
#  <class 'object'>)
Diamond Problem

Python solves this with C3 linearization algorithm:

A
/ \
B C
\ /
D

Multilevel Inheritance

Grandparent
|
Parent
|
Child

Hierarchical Inheritance

Parent
/ \
Child1 Child2

Abstract Base Classes (ABC)

from abc import ABC, abstractmethod

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

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14 * self.radius

# shape = Shape()  # Error: Can't instantiate abstract class
circle = Circle(5)  # OK

2. Polymorphism

Method Overriding

Child classes provide specific implementations of parent methods:

class Bird:
    def fly(self):
        print("Flying high")

class Penguin(Bird):
    def fly(self):
        print("Penguins can't fly!")

birds = [Bird(), Penguin()]
for bird in birds:
    bird.fly()  # Different behavior per type

Duck Typing

Python's approach to polymorphism - objects are used based on behavior, not type:

class Duck:
    def quack(self):
        print("Quack!")

class Person:
    def quack(self):
        print("I'm quacking like a duck!")

def make_it_quack(duck_like_object):
    duck_like_object.quack()

make_it_quack(Duck())   # Quack!
make_it_quack(Person()) # I'm quacking like a duck!

Operator Overloading

Define how operators work with your objects:

Operator Method Description
+ __add__ Addition
- __sub__ Subtraction
* __mul__ Multiplication
== __eq__ Equality
< __lt__ Less than
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)  # Vector(4, 6)
print(v1 == v2) # False

Function Polymorphism

Built-in functions work differently with different object types:

print(len("hello"))    # 5 (string length)
print(len([1,2,3]))   # 3 (list length)
print(len({"a":1})))  # 1 (dict item count)

3. Encapsulation

Access Modifiers (Convention-Based)

Access Level Syntax Description
Public self.value Accessible anywhere
Protected self._value Shouldn't be accessed outside class hierarchy
Private self.__value Name-mangled to prevent accidental access
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # Private
        
    def deposit(self, amount):
        self.__balance += amount
    
    def get_balance(self):
        return self.__balance

account = BankAccount(100)
# account.__balance  # Error (name mangled to _BankAccount__balance)
print(account.get_balance())  # Proper access

Property Decorators

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property
    def celsius(self):
        """Get the temperature in Celsius"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        """Computed property - temperature in Fahrenheit"""
        return (self._celsius * 9/5) + 32

temp = Temperature(25)
print(temp.fahrenheit)  # 77.0
temp.celsius = 30       # Uses setter
# temp.celsius = -300   # Raises ValueError

Why Encapsulation?

  • Data validation: Control how attributes are modified
  • Computed attributes: Derive values from other attributes
  • Implementation hiding: Change internals without breaking client code
  • Access control: Prevent unauthorized access to sensitive data

4. Dunder (Magic) Methods

Object Representation

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Point at ({self.x}, {self.y})"
    
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

p = Point(3, 4)
print(str(p))   # Point at (3, 4) - for users
print(repr(p))  # Point(x=3, y=4) - for developers

Comparison Methods

class Card:
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit
    
    def __eq__(self, other):
        return self.rank == other.rank and self.suit == other.suit
    
    def __lt__(self, other):
        return self.rank < other.rank
    
    def __hash__(self):
        return hash((self.rank, self.suit))

queen_hearts = Card(12, 'hearts')
king_spades = Card(13, 'spades')
print(queen_hearts < king_spades)  # True

Arithmetic Operations

class Fraction:
    def __init__(self, numerator, denominator):
        self.n = numerator
        self.d = denominator
    
    def __add__(self, other):
        new_n = self.n * other.d + other.n * self.d
        new_d = self.d * other.d
        return Fraction(new_n, new_d)
    
    def __iadd__(self, other):
        self.n = self.n * other.d + other.n * self.d
        self.d = self.d * other.d
        return self
    
    def __str__(self):
        return f"{self.n}/{self.d}"

f1 = Fraction(1, 2)
f2 = Fraction(1, 3)
print(f1 + f2)  # 5/6
f1 += f2
print(f1)       # 5/6

Container Methods

class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def __len__(self):
        return len(self.items)
    
    def __getitem__(self, index):
        return self.items[index]
    
    def __setitem__(self, index, value):
        self.items[index] = value
    
    def add_item(self, item):
        self.items.append(item)

cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
print(len(cart))    # 2
print(cart[1])      # Banana
cart[1] = "Orange"  # Uses __setitem__

Callable Objects

class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, x):
        return x * self.factor

double = Multiplier(2)
print(double(5))  # 10 - instance called like a function

Context Managers

class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.end = time.time()
        print(f"Elapsed: {self.end - self.start:.2f}s")

with Timer() as t:
    # Code to time
    sum(range(1000000))
# Output: Elapsed: 0.05s

5. Advanced Patterns & Best Practices

Mixins

Reusable components in multiple inheritance:

class JsonMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class XmlMixin:
    def to_xml(self):
        # Simple XML conversion
        items = [f"<{k}>{v}</{k}>" for k, v in self.__dict__.items()]
        return f"<object>{''.join(items)}</object>"

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

p = Person("Alice", 25)
print(p.to_json())  # {"name": "Alice", "age": 25}
print(p.to_xml())   # <object><name>Alice</name><age>25</age></object>

Descriptors

Powerful attribute access control:

class PositiveNumber:
    def __set_name__(self, owner, name):
        self.name = name
    
    def __get__(self, obj, objtype=None):
        return obj.__dict__.get(self.name)
    
    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("Must be positive")
        obj.__dict__[self.name] = value

class Circle:
    radius = PositiveNumber()  # Descriptor instance
    
    def __init__(self, radius):
        self.radius = radius  # Uses __set__

c = Circle(5)
# c.radius = -1  # Raises ValueError

Metaclasses

Customize class creation:

class SingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Singleton(metaclass=SingletonMeta):
    pass

a = Singleton()
b = Singleton()
print(a is b)  # True

Dependency Injection

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

class Car:
    def __init__(self, engine):  # Constructor injection
        self.engine = engine
    
    def start(self):
        self.engine.start()

# Usage
engine = Engine()
car = Car(engine)  # Dependency injected
car.start()

6. Performance & Optimization

__slots__

Reduce memory usage by preventing dynamic attribute creation:

class Point:
    __slots__ = ['x', 'y']  # Only these attributes allowed
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(3, 4)
# p.z = 5  # AttributeError (no __dict__ by default)

Note: __slots__ trades flexibility for memory savings. Use when you have many instances and fixed attributes.

Method Lookup Optimization

Python caches method lookups, but multiple inheritance can impact performance:

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass  # More complex MRO

# Method lookup is slower in D than in single inheritance

Weak References

Break circular references without preventing garbage collection:

import weakref

class Node:
    def __init__(self, value):
        self.value = value
        self._parent = None
        self.children = []
    
    @property
    def parent(self):
        return self._parent() if self._parent else None
    
    @parent.setter
    def parent(self, node):
        self._parent = weakref.ref(node)  # Weak reference

parent = Node("parent")
child = Node("child")
child.parent = parent
parent.children.append(child)

Complete Example: Advanced OOP Implementation

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List

class Animal(ABC):
    """Abstract base class for all animals"""
    @abstractmethod
    def speak(self) -> str:
        """Return the sound the animal makes"""
        pass
    
    @property
    @abstractmethod
    def scientific_name(self) -> str:
        """Return the animal's scientific name"""
        pass

@dataclass
class Dog(Animal):
    """A dog that can bark"""
    name: str
    breed: str
    
    def speak(self) -> str:
        return f"{self.name} says Woof!"
    
    @property
    def scientific_name(self) -> str:
        return "Canis lupus familiaris"
    
    def __str__(self) -> str:
        return f"I am a {self.breed} named {self.name}"
    
    def __repr__(self) -> str:
        return f"Dog(name='{self.name}', breed='{self.breed}')"

class PetShop:
    """A shop that sells pets with encapsulation"""
    def __init__(self):
        self.__pets: List[Animal] = []  # Private list
    
    @property
    def pets(self) -> List[Animal]:
        """Get a copy of the pets list"""
        return self.__pets.copy()
    
    def add_pet(self, pet: Animal) -> None:
        """Add a pet to the shop"""
        if isinstance(pet, Animal):
            self.__pets.append(pet)
        else:
            raise TypeError("Only Animal instances allowed")
    
    def __len__(self) -> int:
        """Number of pets in the shop"""
        return len(self.__pets)
    
    def __getitem__(self, index: int) -> Animal:
        """Get pet by index"""
        return self.__pets[index]

# Usage
shop = PetShop()
shop.add_pet(Dog("Buddy", "Golden Retriever"))
shop.add_pet(Dog("Max", "Beagle"))

print([pet.speak() for pet in shop.pets])  # Polymorphism
print(len(shop))  # Uses __len__
print(shop[1])    # Uses __getitem__

Post a Comment

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.