Classes and Objects
class Dog:
# Class variable (shared by all instances)
species = "Canis familiaris"
# Constructor
def __init__(self, name, age):
# Instance variables
self.name = name
self.age = age
# Instance method
def bark(self):
return f"{self.name} says: Woof!"
def __str__(self):
return f"Dog({self.name}, age={self.age})"
def __repr__(self):
return f"Dog(name={self.name!r}, age={self.age!r})"
# Creating instances
rex = Dog("Rex", 3)
buddy = Dog("Buddy", 5)
print(rex.bark()) # Rex says: Woof!
print(rex.species) # Canis familiaris
print(str(rex)) # Dog(Rex, age=3)
print(Dog.species) # Canis familiaris โ access via class
Inheritance
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement speak()")
def describe(self):
return f"I am {self.name}"
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class Duck(Animal):
def speak(self):
return "Quack!"
# Polymorphism
animals = [Dog("Rex"), Cat("Whiskers"), Duck("Donald")]
for animal in animals:
print(f"{animal.name}: {animal.speak()}")
super()
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def info(self):
return f"{self.year} {self.make} {self.model}"
class ElectricCar(Vehicle):
def __init__(self, make, model, year, battery_kwh):
super().__init__(make, model, year) # call parent __init__
self.battery_kwh = battery_kwh
def info(self):
base = super().info()
return f"{base} (Electric, {self.battery_kwh}kWh)"
tesla = ElectricCar("Tesla", "Model 3", 2023, 82)
print(tesla.info()) # 2023 Tesla Model 3 (Electric, 82kWh)
Multiple Inheritance and MRO
class A:
def hello(self):
return "Hello from A"
class B(A):
def hello(self):
return "Hello from B"
class C(A):
def hello(self):
return "Hello from C"
class D(B, C):
pass
d = D()
print(d.hello()) # "Hello from B" โ follows MRO
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, ...)
Encapsulation
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # "protected" โ convention
self.__pin = "1234" # "private" โ name mangling
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, amount):
if amount < 0:
raise ValueError("Balance cannot be negative")
self._balance = amount
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
return self._balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
return self._balance
account = BankAccount("Alice", 1000)
print(account.balance) # 1000 โ uses @property getter
account.balance = 2000 # uses @property setter
account.deposit(500)
print(account.balance) # 2500
Class and Static Methods
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
@classmethod
def from_fahrenheit(cls, f):
"""Alternative constructor"""
return cls((f - 32) * 5/9)
@staticmethod
def is_freezing(celsius):
"""Doesn't need self or cls"""
return celsius <= 0
boiling = Temperature(100)
print(boiling.fahrenheit) # 212.0
body_temp = Temperature.from_fahrenheit(98.6)
print(round(body_temp.celsius, 1)) # 37.0
print(Temperature.is_freezing(-5)) # True
Abstract Classes
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def describe(self):
return f"Area={self.area():.2f}, Perimeter={self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
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)
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
print(shape.describe())
# Area=78.54, Perimeter=31.42
# Area=24.00, Perimeter=20.00
Dunder Methods
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 __abs__(self):
return (self.x**2 + self.y**2)**0.5
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(v1 + v2) # Vector(3, 7)
print(v1 * 2) # Vector(4, 6)
print(abs(v1)) # 3.605...
Exercises
Exercise 1: Stack Class
Implement a Stack class with push, pop, peek, is_empty, and size methods.
Solution:
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._data.pop()
def peek(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._data[-1]
def is_empty(self):
return len(self._data) == 0
def size(self):
return len(self._data)
def __repr__(self):
return f"Stack({self._data})"
s = Stack()
s.push(1)
s.push(2)
s.push(3)
print(s.pop()) # 3
print(s.peek()) # 2
print(s.size()) # 2
Exercise 2: Linked List
Build a simple singly linked list with append, prepend, delete, and display.
Solution:
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
node = Node(data)
if not self.head:
self.head = node
return
current = self.head
while current.next:
current = current.next
current.next = node
def prepend(self, data):
node = Node(data)
node.next = self.head
self.head = node
def delete(self, data):
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next:
if current.next.data == data:
current.next = current.next.next
return
current = current.next
def __repr__(self):
items = []
current = self.head
while current:
items.append(str(current.data))
current = current.next
return " -> ".join(items)
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.prepend(0)
print(ll) # 0 -> 1 -> 2 -> 3
ll.delete(2)
print(ll) # 0 -> 1 -> 3