Python - Classes and Objects Tutorial
• Classes define blueprints for objects with attributes (data) and methods (behavior), enabling organized, reusable code through encapsulation and abstraction
Key Insights
• Classes define blueprints for objects with attributes (data) and methods (behavior), enabling organized, reusable code through encapsulation and abstraction
• Instance methods operate on object data using self, while class methods use cls for class-level operations, and static methods provide utility functions without accessing class or instance state
• Python’s special methods (__init__, __str__, __repr__) control object lifecycle and behavior, while properties enable controlled attribute access with validation logic
Understanding Classes and Objects
Classes serve as templates for creating objects. An object is an instance of a class containing both data (attributes) and functions (methods) that operate on that data.
class BankAccount:
def __init__(self, account_number, balance=0):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
if amount > 0:
self.balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self.balance:
self.balance -= amount
return True
return False
# Creating objects
account1 = BankAccount("ACC001", 1000)
account2 = BankAccount("ACC002")
account1.deposit(500)
print(account1.balance) # 1500
The __init__ method initializes object state when creating instances. The self parameter references the current instance, allowing methods to access and modify object attributes.
Instance, Class, and Static Methods
Python supports three method types, each serving distinct purposes.
class Employee:
company_name = "TechCorp" # Class attribute
employee_count = 0
def __init__(self, name, salary):
self.name = name
self.salary = salary
Employee.employee_count += 1
# Instance method
def give_raise(self, amount):
self.salary += amount
return self.salary
# Class method
@classmethod
def change_company_name(cls, new_name):
cls.company_name = new_name
@classmethod
def from_string(cls, emp_string):
name, salary = emp_string.split('-')
return cls(name, int(salary))
# Static method
@staticmethod
def is_workday(day):
return day not in ['Saturday', 'Sunday']
# Usage
emp1 = Employee("Alice", 75000)
emp1.give_raise(5000)
# Alternative constructor using class method
emp2 = Employee.from_string("Bob-80000")
# Class method modifies class state
Employee.change_company_name("InnovateTech")
# Static method doesn't access instance or class
print(Employee.is_workday("Monday")) # True
Instance methods access instance data via self. Class methods receive cls and operate on class-level data. Static methods are utility functions that don’t require access to class or instance state.
Special Methods (Magic Methods)
Special methods customize object behavior for built-in operations.
class Product:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def __str__(self):
# Human-readable string representation
return f"{self.name} - ${self.price}"
def __repr__(self):
# Developer-friendly representation
return f"Product('{self.name}', {self.price}, {self.quantity})"
def __eq__(self, other):
# Define equality comparison
if isinstance(other, Product):
return self.name == other.name and self.price == other.price
return False
def __lt__(self, other):
# Define less-than comparison for sorting
return self.price < other.price
def __add__(self, other):
# Define addition behavior
if isinstance(other, Product) and self.name == other.name:
return Product(self.name, self.price, self.quantity + other.quantity)
raise ValueError("Cannot add different products")
# Usage
p1 = Product("Laptop", 999, 5)
p2 = Product("Laptop", 999, 3)
print(str(p1)) # Laptop - $999
print(repr(p1)) # Product('Laptop', 999, 5)
print(p1 == p2) # True
p3 = p1 + p2
print(p3.quantity) # 8
Common special methods include __init__, __str__, __repr__, __eq__, __lt__, __add__, __len__, and __getitem__.
Properties and Encapsulation
Properties provide controlled access to attributes with validation and computed values.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
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):
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
# Usage
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 86
print(temp.celsius) # 30.0
# Validation in action
try:
temp.celsius = -300
except ValueError as e:
print(e) # Temperature below absolute zero
The @property decorator creates getter methods, while @<property>.setter creates setters. This pattern enables attribute access syntax while maintaining validation logic.
Inheritance and Polymorphism
Inheritance allows classes to extend functionality from parent classes.
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
self.mileage = 0
def drive(self, distance):
self.mileage += distance
return f"Drove {distance} miles"
def info(self):
return f"{self.brand} {self.model}"
class ElectricVehicle(Vehicle):
def __init__(self, brand, model, battery_capacity):
super().__init__(brand, model)
self.battery_capacity = battery_capacity
self.charge_level = 100
def drive(self, distance):
charge_needed = distance * 0.3
if self.charge_level >= charge_needed:
self.charge_level -= charge_needed
return super().drive(distance)
return "Insufficient charge"
def charge(self, amount):
self.charge_level = min(100, self.charge_level + amount)
class GasVehicle(Vehicle):
def __init__(self, brand, model, tank_size):
super().__init__(brand, model)
self.tank_size = tank_size
self.fuel_level = tank_size
def drive(self, distance):
fuel_needed = distance * 0.05
if self.fuel_level >= fuel_needed:
self.fuel_level -= fuel_needed
return super().drive(distance)
return "Insufficient fuel"
def refuel(self):
self.fuel_level = self.tank_size
# Polymorphism in action
vehicles = [
ElectricVehicle("Tesla", "Model 3", 75),
GasVehicle("Toyota", "Camry", 15)
]
for vehicle in vehicles:
print(vehicle.info())
print(vehicle.drive(50))
The super() function calls parent class methods. Polymorphism allows different classes to implement the same interface differently, enabling flexible code that works with various object types.
Composition Over Inheritance
Composition builds complex objects by combining simpler ones, offering greater flexibility than inheritance.
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
self.running = False
def start(self):
self.running = True
return "Engine started"
def stop(self):
self.running = False
return "Engine stopped"
class GPS:
def __init__(self):
self.location = (0, 0)
def navigate(self, destination):
return f"Navigating to {destination}"
class Car:
def __init__(self, brand, engine, has_gps=False):
self.brand = brand
self.engine = engine
self.gps = GPS() if has_gps else None
def start(self):
return self.engine.start()
def navigate(self, destination):
if self.gps:
return self.gps.navigate(destination)
return "No GPS available"
# Usage
engine = Engine(300)
car = Car("BMW", engine, has_gps=True)
print(car.start()) # Engine started
print(car.navigate("Downtown")) # Navigating to Downtown
Composition creates “has-a” relationships rather than “is-a” relationships, making code more modular and easier to modify.