Chapter 7: Object-Oriented Programming
Classes and Objects
If a "class" is a cookie cutter, then an "object" is the cookie pressed out from that cutter. The cutter defines the shape, but each cookie is an independent individual.
bash
# Define a "Dog" class
class Dog:
# Class variable: shared by all instances
species = "Canine"
# __init__ is the constructor, automatically called when creating an object
def __init__(self, name, age, breed):
# Instance variables: unique to each instance
self.name = name
self.age = age
self.breed = breed
# Instance method
def bark(self):
return f"🐕 {self.name} says: Woof!"
def info(self):
return f"{self.name} is a {self.breed}, {self.age} years old"
# Create objects (instantiate)
dog1 = Dog("Rex", 3, "Golden Retriever")
dog2 = Dog("Buddy", 5, "Corgi")
print(dog1.bark())
print(dog2.info())
print(f"Breed: {dog1.species}")bash
🐕 Rex says: Woof!
Buddy is a Corgi, 5 years old
Breed: CanineInheritance
bash
# Parent class
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f"{self.name} says: {self.sound}!"
# Child class inherits from parent
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name, sound="Meow") # Call parent constructor
self.indoor = indoor
def purr(self): # Method unique to child class
return f"{self.name} is purring... purr purr 😺"
class Duck(Animal):
def __init__(self, name):
super().__init__(name, sound="Quack")
def swim(self):
return f"{self.name} is swimming 🏊"
cat = Cat("Whiskers")
duck = Duck("Donald")
print(cat.speak()) # Inherited method
print(cat.purr()) # Child class method
print(duck.speak())
print(duck.swim())bash
Whiskers says: Meow!
Whiskers is purring... purr purr 😺
Donald says: Quack!
Donald is swimming 🏊Polymorphism
bash
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement the area method")
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
# Polymorphism: same interface, different behavior
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]
for shape in shapes:
print(f"{shape.__class__.__name__} area = {shape.area():.2f}")bash
Circle area = 78.54
Rectangle area = 24.00
Circle area = 28.27Encapsulation and Magic Methods
bash
# Encapsulation: use _ and __ to control access
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner # Public attribute
self._bank = "Python Bank" # Convention for private (just advisory, still accessible)
self.__balance = balance # Name mangling (more strict)
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited {amount}, balance is {self.__balance}"
return "Deposit amount must be positive"
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return f"Withdrew {amount}, balance is {self.__balance}"
return "Insufficient balance"
# Magic methods: give objects special behavior
def __str__(self):
# Called by print()
return f"[{self.owner}'s Account] Balance: {self.__balance}"
def __repr__(self):
# Called during debugging
return f"BankAccount('{self.owner}', {self.__balance})"
def __len__(self):
# Called by len(), returns the "digit count" of the balance
return len(str(self.__balance))
account = BankAccount("Alice", 1000)
print(account)
print(account.deposit(500))
print(account.withdraw(200))
print(repr(account))
print(f"Balance digits: {len(account)}")bash
[Alice's Account] Balance: 1000
Deposited 500, balance is 1500
Withdrew 200, balance is 1300
BankAccount('Alice', 1300)
Balance digits: 4