Object-Oriented Programming
Introduction
Object-Oriented Programming (OOP) is a paradigm that organizes code around objects - entities that combine data (attributes) and behavior (methods). It's one of the most influential programming paradigms, used in languages from Python to Java to C++.
Learning Objectives
By the end of this reading, you will be able to:
- Create classes and instantiate objects
- Define attributes and methods
- Understand encapsulation, inheritance, and polymorphism
- Use special methods (dunder methods)
- Apply OOP design principles
1. Classes and Objects
What is a Class?
A class is a blueprint for creating objects. It defines:
- Attributes: Data the object holds
- Methods: Functions the object can perform
A class is like a cookie cutter; objects are the cookies.
What is an Object?
An object is an instance of a class - a concrete realization with actual data.
# Class definition
class Dog:
pass
# Creating objects (instances)
my_dog = Dog()
your_dog = Dog()
# Each object is independent
print(my_dog) # <__main__.Dog object at 0x...>
print(your_dog) # <__main__.Dog object at 0x...> (different address)
2. Defining Classes
Basic Class Structure
class Dog:
# Class attribute (shared by all instances)
species = "Canis familiaris"
# Initializer (constructor)
def __init__(self, name, age):
# Instance attributes (unique to each instance)
self.name = name
self.age = age
# Instance method
def bark(self):
print(f"{self.name} says woof!")
# Another instance method
def description(self):
return f"{self.name} is {self.age} years old"
The __init__ Method
Called automatically when creating a new object:
# Creating instances
buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)
print(buddy.name) # "Buddy"
print(max_dog.age) # 5
The self Parameter
self refers to the instance calling the method:
buddy.bark() # "Buddy says woof!"
max_dog.bark() # "Max says woof!"
# Internally, Python translates this to:
# Dog.bark(buddy)
# Dog.bark(max_dog)
Class vs Instance Attributes
class Dog:
species = "Canis familiaris" # Class attribute
def __init__(self, name):
self.name = name # Instance attribute
buddy = Dog("Buddy")
max_dog = Dog("Max")
# Instance attributes are unique
print(buddy.name) # "Buddy"
print(max_dog.name) # "Max"
# Class attributes are shared
print(buddy.species) # "Canis familiaris"
print(max_dog.species) # "Canis familiaris"
# Changing class attribute affects all instances
Dog.species = "Canis lupus"
print(buddy.species) # "Canis lupus"
3. Encapsulation
Encapsulation bundles data and methods, controlling access to internal state.
Public, Protected, Private (Convention)
Python doesn't enforce access control, but uses naming conventions:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public
self._balance = balance # Protected (convention: internal use)
self.__pin = "1234" # Private (name mangling)
def deposit(self, amount):
if amount > 0:
self._balance += amount
def get_balance(self):
return self._balance
account = BankAccount("Alice", 100)
# Public - accessible
print(account.owner) # "Alice"
# Protected - accessible but discouraged
print(account._balance) # 100 (works, but not recommended)
# Private - name mangled
# print(account.__pin) # AttributeError
print(account._BankAccount__pin) # "1234" (still accessible, but ugly)
Properties
Control attribute access with getters and setters:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Getter for radius"""
return self._radius
@radius.setter
def radius(self, value):
"""Setter for radius with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""Computed property"""
return 3.14159 * self._radius ** 2
circle = Circle(5)
print(circle.radius) # 5 (uses getter)
print(circle.area) # 78.54 (computed)
circle.radius = 10 # Uses setter
# circle.radius = -1 # Raises ValueError
4. Inheritance
Inheritance allows a class to inherit attributes and methods from another class.
Basic Inheritance
# Parent class (base class)
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement")
# Child class (derived class)
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")
whiskers = Cat("Whiskers")
print(buddy.speak()) # "Buddy says Woof!"
print(whiskers.speak()) # "Whiskers says Meow!"
The super() Function
Call parent class methods:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # Call parent __init__
self.breed = breed
buddy = Dog("Buddy", 3, "Golden Retriever")
print(buddy.name) # "Buddy"
print(buddy.breed) # "Golden Retriever"
Method Overriding
Child classes can override parent methods:
class Animal:
def make_sound(self):
return "Some generic sound"
class Dog(Animal):
def make_sound(self):
return "Woof!"
animal = Animal()
dog = Dog()
print(animal.make_sound()) # "Some generic sound"
print(dog.make_sound()) # "Woof!"
Multiple Inheritance
Python supports inheriting from multiple classes:
class Flyable:
def fly(self):
return "Flying!"
class Swimmable:
def swim(self):
return "Swimming!"
class Duck(Flyable, Swimmable):
def quack(self):
return "Quack!"
duck = Duck()
print(duck.fly()) # "Flying!"
print(duck.swim()) # "Swimming!"
print(duck.quack()) # "Quack!"
Method Resolution Order (MRO)
Python uses MRO to determine which method to call:
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):
pass
d = D()
print(d.method()) # "B" (B comes before C in D's inheritance)
print(D.__mro__) # Shows: D -> B -> C -> A -> object
5. Polymorphism
Polymorphism allows different classes to be treated through a common interface.
Duck Typing
"If it walks like a duck and quacks like a duck, it's a duck."
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Duck:
def speak(self):
return "Quack!"
def animal_sound(animal):
# Works with any object that has a speak() method
print(animal.speak())
animal_sound(Dog()) # "Woof!"
animal_sound(Cat()) # "Meow!"
animal_sound(Duck()) # "Quack!"
Abstract Base Classes
Enforce that subclasses implement certain methods:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
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() # TypeError: Can't instantiate abstract class
rect = Rectangle(5, 3)
print(rect.area()) # 15
print(rect.perimeter()) # 16
6. Special Methods (Dunder Methods)
Special methods customize object behavior. They're surrounded by double underscores.
Object Representation
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""Human-readable string (for print)"""
return f"Point({self.x}, {self.y})"
def __repr__(self):
"""Developer-friendly string (for debugging)"""
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 4)
print(p) # Point(3, 4) - uses __str__
print(repr(p)) # Point(x=3, y=4) - uses __repr__
Comparison Methods
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
def __le__(self, other):
return self.age <= other.age
alice = Person("Alice", 25)
bob = Person("Bob", 30)
print(alice == bob) # False
print(alice < bob) # True
print(alice <= bob) # True
Arithmetic 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 __sub__(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 __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
Container Methods
class Playlist:
def __init__(self):
self.songs = []
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
def __setitem__(self, index, value):
self.songs[index] = value
def __contains__(self, song):
return song in self.songs
def __iter__(self):
return iter(self.songs)
def add(self, song):
self.songs.append(song)
playlist = Playlist()
playlist.add("Song A")
playlist.add("Song B")
playlist.add("Song C")
print(len(playlist)) # 3
print(playlist[1]) # "Song B"
print("Song A" in playlist) # True
for song in playlist:
print(song)
Context Managers
class FileHandler:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
return False # Don't suppress exceptions
# Usage
with FileHandler("test.txt", "w") as f:
f.write("Hello!")
# File is automatically closed after the block
7. Class Methods and Static Methods
Instance Methods
Regular methods - operate on instances:
class Dog:
def bark(self): # self = instance
print("Woof!")
Class Methods
Operate on the class itself:
class Dog:
count = 0
def __init__(self, name):
self.name = name
Dog.count += 1
@classmethod
def get_count(cls): # cls = class
return cls.count
@classmethod
def create_puppy(cls, name):
"""Alternative constructor"""
return cls(f"Baby {name}")
print(Dog.get_count()) # 0
dog1 = Dog("Buddy")
dog2 = Dog("Max")
print(Dog.get_count()) # 2
puppy = Dog.create_puppy("Rex")
print(puppy.name) # "Baby Rex"
Static Methods
Don't access instance or class - utility functions:
class MathUtils:
@staticmethod
def is_even(n):
return n % 2 == 0
@staticmethod
def factorial(n):
if n <= 1:
return 1
return n * MathUtils.factorial(n - 1)
print(MathUtils.is_even(4)) # True
print(MathUtils.factorial(5)) # 120
8. Composition vs Inheritance
Inheritance: "is-a" Relationship
class Animal:
pass
class Dog(Animal): # Dog IS-A Animal
pass
Composition: "has-a" Relationship
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine() # Car HAS-A Engine
def start(self):
return self.engine.start()
car = Car()
print(car.start()) # "Engine started"
Prefer composition over inheritance when possible - it's more flexible.
9. Design Principles
Single Responsibility Principle
A class should have one reason to change.
# Bad - multiple responsibilities
class User:
def __init__(self, name):
self.name = name
def save_to_database(self):
pass # Database logic
def send_email(self):
pass # Email logic
# Good - separated responsibilities
class User:
def __init__(self, name):
self.name = name
class UserRepository:
def save(self, user):
pass
class EmailService:
def send(self, user, message):
pass
Open/Closed Principle
Open for extension, closed for modification.
# Good - extend through inheritance, not modification
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def area(self):
return self.width * self.height
class Circle(Shape):
def area(self):
return 3.14 * self.radius ** 2
# Adding new shapes doesn't modify existing code
Exercises
Basic
Create a
Rectangleclass withwidthandheightattributes, and methodsarea()andperimeter().Create a
BankAccountclass with methodsdeposit(),withdraw(), andget_balance(). Ensure balance can't go negative.Create a
Studentclass withnameandgrades(list). Add methodaverage_grade().
Intermediate
Create a class hierarchy:
Vehicle(base) withmake,model,yearCar(inherits Vehicle) addsnum_doorsMotorcycle(inherits Vehicle) addshas_sidecar
Implement a
Fractionclass with:__init__,__str____add__,__sub__,__mul____eq__,__lt__
Create a
Deckclass that:- Contains 52 cards
- Has
shuffle()method - Has
deal()method that removes and returns a card - Supports
len(deck)
Advanced
Implement a
LinkedListclass with:- Node class (internal)
append(),prepend(),delete()__len__,__iter__,__contains__
Create a plugin system using abstract base classes where plugins must implement
execute()andget_name().Implement the Observer pattern: subjects notify observers of state changes.
Summary
- Classes are blueprints; objects are instances
__init__initializes objects;selfrefers to the instance- Encapsulation bundles data and methods, controls access
- Inheritance creates "is-a" relationships
- Polymorphism allows treating different types uniformly
- Special methods customize object behavior
- Composition often preferable to inheritance
- Follow design principles for maintainable code