Advanced Object-Oriented Programming in Python
Mastering inheritance, polymorphism, encapsulation, and Python's advanced OOP features
Table of Contents
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
|
Child
Multiple Inheritance
\ /
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:
/ \
B C
\ /
D
Multilevel Inheritance
|
Parent
|
Child
Hierarchical Inheritance
/ \
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__