Object-Oriented Programming
Classes, objects, and OOP principles in Python.
Classes and Objects
Basic Class
class Dog:
"""A simple dog class."""
species = "Canis familiaris" # Class attribute (shared)
def __init__(self, name, age):
"""Initialize instance attributes."""
self.name = name # Instance attribute
self.age = age
def bark(self):
"""Instance method."""
return f"{self.name} says woof!"
def description(self):
return f"{self.name} is {self.age} years old"
# Create instances
buddy = Dog("Buddy", 5)
miles = Dog("Miles", 3)
# Access attributes
buddy.name # "Buddy"
buddy.species # "Canis familiaris"
Dog.species # "Canis familiaris"
# Call methods
buddy.bark() # "Buddy says woof!"
The init Method
Constructor - runs when creating an instance:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
self.area = width * height # Computed at init
def __init__(self, width, height=None):
"""Optional parameter - square if height not given."""
self.width = width
self.height = height or width
Instance vs Class Attributes
class Counter:
count = 0 # Class attribute - shared by all instances
def __init__(self):
Counter.count += 1
self.id = Counter.count # Instance attribute
c1 = Counter() # Counter.count = 1, c1.id = 1
c2 = Counter() # Counter.count = 2, c2.id = 2
c3 = Counter() # Counter.count = 3, c3.id = 3
Methods
Instance Methods
class Dog:
def __init__(self, name):
self.name = name
def bark(self): # self = the instance
return f"{self.name} barks"
Class Methods
Receive the class, not instance:
class Dog:
count = 0
def __init__(self, name):
self.name = name
Dog.count += 1
@classmethod
def get_count(cls):
return cls.count
@classmethod
def from_string(cls, dog_string):
"""Alternative constructor."""
name, age = dog_string.split("-")
return cls(name)
Dog.get_count() # 0
buddy = Dog("Buddy")
Dog.get_count() # 1
miles = Dog.from_string("Miles-3")
Static Methods
No access to instance or class:
class Math:
@staticmethod
def add(a, b):
return a + b
Math.add(2, 3) # 5
Properties
Computed attributes with getter/setter:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Getter."""
return self._radius
@radius.setter
def radius(self, value):
"""Setter with validation."""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""Read-only computed property."""
return 3.14159 * self._radius ** 2
c = Circle(5)
c.radius # 5 (calls getter)
c.radius = 10 # (calls setter)
c.area # 314.159 (computed)
# c.area = 100 # Error - no setter
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!"
buddy = Dog("Buddy")
buddy.speak() # "Buddy says woof!"
super()
Call parent class methods:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Call parent __init__
self.breed = breed
Method Resolution Order (MRO)
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C): # Multiple inheritance
pass
d = D()
d.method() # "B" - B comes before C
# Check MRO
D.__mro__
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
isinstance and issubclass
buddy = Dog("Buddy")
isinstance(buddy, Dog) # True
isinstance(buddy, Animal) # True
isinstance(buddy, Cat) # False
issubclass(Dog, Animal) # True
issubclass(Dog, Cat) # False
Dunder (Magic) Methods
Special methods with double underscores:
String Representation
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""Human-readable (print, str())."""
return f"({self.x}, {self.y})"
def __repr__(self):
"""Developer-readable (debugging, repr())."""
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p) # (3, 4)
repr(p) # Point(3, 4)
Comparison
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
# Use @functools.total_ordering to auto-generate
# __le__, __gt__, __ge__ from __eq__ and __lt__
Arithmetic
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 __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # scalar * vector
return self.__mul__(scalar)
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2 # Vector(4, 6)
v4 = v1 * 2 # Vector(2, 4)
v5 = 2 * v1 # Vector(2, 4) - uses __rmul__
Container Emulation
class MyList:
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 __contains__(self, item):
return item in self._items
def __iter__(self):
return iter(self._items)
Context Manager
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.elapsed = time.time() - self.start
print(f"Elapsed: {self.elapsed:.2f}s")
return False # Don't suppress exceptions
with Timer() as t:
# do something
pass
Callable Objects
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
double(5) # 10
triple = Multiplier(3)
triple(5) # 15
Dataclasses (3.7+)
Reduce boilerplate for data-holding classes:
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
label: str = "origin" # Default value
# Automatically generates:
# __init__, __repr__, __eq__
p = Point(3, 4)
print(p) # Point(x=3, y=4, label='origin')
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False) # Computed, not in __init__
def __post_init__(self):
self.area = self.width * self.height
@dataclass(frozen=True) # Immutable
class FrozenPoint:
x: float
y: float
Dataclass Options
@dataclass(
init=True, # Generate __init__
repr=True, # Generate __repr__
eq=True, # Generate __eq__
order=False, # Generate comparison methods
frozen=False, # Make immutable
slots=False, # Use __slots__ (3.10+)
)
class MyClass:
pass
Encapsulation
Convention-Based Privacy
class Account:
def __init__(self, balance):
self._balance = balance # "Protected" - single underscore
self.__secret = "hidden" # "Private" - name mangled
def get_balance(self):
return self._balance
a = Account(100)
a._balance # Works (but discouraged)
a.__secret # AttributeError
a._Account__secret # Works (name mangling)
Using Properties for Encapsulation
class BankAccount:
def __init__(self, initial_balance=0):
self._balance = initial_balance
self._transactions = []
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
self._transactions.append(f"+{amount}")
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
self._transactions.append(f"-{amount}")
Abstract Base Classes
Define interfaces:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Calculate area."""
pass
@abstractmethod
def perimeter(self):
"""Calculate perimeter."""
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# shape = Shape() # Error - can't instantiate ABC
rect = Rectangle(3, 4) # OK
Composition vs Inheritance
Prefer composition ("has-a") over inheritance ("is-a"):
# Inheritance - tight coupling
class Dog(Animal):
pass
# Composition - loose coupling, more flexible
class Dog:
def __init__(self, name):
self.name = name
self.legs = Legs(4)
self.voice = Voice("bark")
def speak(self):
return self.voice.make_sound()
Practice
# 1. Basic class with properties
@dataclass
class Product:
name: str
price: float
quantity: int = 0
@property
def total_value(self):
return self.price * self.quantity
# 2. Inheritance
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_pay(self):
return self.salary
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def get_pay(self):
return self.salary + self.bonus
# 3. Context manager
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
def __enter__(self):
print(f"Connecting to {self.db_name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing connection to {self.db_name}")
return False
with DatabaseConnection("mydb") as conn:
print("Doing database stuff")