Classes & Object-Oriented Programming

What are Classes?

Classes are blueprints for creating objects. They define attributes (data) and methods (functions) that objects will have.

# Basic class definition
class Person:
    def __init__(self, name, age):
        self.name = name  # Instance attribute
        self.age = age
    
    def introduce(self):  # Instance method
        return f"Hi, I'm {name}, {age} years old"

# Creating objects (instances)
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

print(person1.name)        # Alice
print(person1.introduce()) # Hi, I'm Alice, 25 years old

Class vs Instance Attributes

class Student:
    # Class attribute (shared by all instances)
    school_name = "Python High School"
    total_students = 0
    
    def __init__(self, name, grade):
        # Instance attributes (unique to each instance)
        self.name = name
        self.grade = grade
        Student.total_students += 1  # Increment class attribute
    
    def study(self):
        return f"{self.name} is studying"
    
    @classmethod
    def get_school_info(cls):
        return f"School: {cls.school_name}, Students: {cls.total_students}"

# Creating instances
student1 = Student("Alice", "A")
student2 = Student("Bob", "B")

print(Student.school_name)      # Python High School
print(student1.school_name)     # Python High School (inherited)
print(Student.get_school_info()) # School: Python High School, Students: 2

Inheritance

# Base class (parent)
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    def make_sound(self):
        return "Some generic animal sound"
    
    def info(self):
        return f"{self.name} is a {self.species}"

# Derived class (child)
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Dog")  # Call parent constructor
        self.breed = breed
    
    def make_sound(self):  # Override parent method
        return "Woof!"
    
    def fetch(self):  # New method specific to Dog
        return f"{self.name} is fetching the ball"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, "Cat")
        self.color = color
    
    def make_sound(self):
        return "Meow!"
    
    def climb(self):
        return f"{self.name} is climbing a tree"

# Using inheritance
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")

print(dog.info())        # Buddy is a Dog
print(dog.make_sound())  # Woof!
print(dog.fetch())       # Buddy is fetching the ball

print(cat.info())        # Whiskers is a Cat
print(cat.make_sound())  # Meow!

Encapsulation and Privacy

class BankAccount:
    def __init__(self, account_number, initial_balance=0):
        self.account_number = account_number
        self._balance = initial_balance  # Protected attribute (convention)
        self.__pin = "1234"             # Private attribute (name mangling)
    
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            return f"Deposited ${amount}. New balance: ${self._balance}"
        return "Invalid deposit amount"
    
    def withdraw(self, amount, pin):
        if not self.__verify_pin(pin):
            return "Invalid PIN"
        if amount > self._balance:
            return "Insufficient funds"
        self._balance -= amount
        return f"Withdrew ${amount}. New balance: ${self._balance}"
    
    def get_balance(self, pin):
        if self.__verify_pin(pin):
            return f"Current balance: ${self._balance}"
        return "Invalid PIN"
    
    def __verify_pin(self, pin):  # Private method
        return pin == self.__pin

# Using the class
account = BankAccount("12345", 1000)
print(account.deposit(500))           # Deposited $500. New balance: $1500
print(account.withdraw(200, "1234"))  # Withdrew $200. New balance: $1300
print(account.get_balance("1234"))    # Current balance: $1300

# These would not work as expected:
# print(account.__pin)        # AttributeError
# print(account.__verify_pin("1234"))  # AttributeError

Polymorphism

# Polymorphism allows objects of different classes to be treated uniformly
class Shape:
    def area(self):
        pass
    
    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)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Polymorphism in action
shapes = [
    Rectangle(5, 3),
    Circle(4),
    Rectangle(2, 8)
]

for shape in shapes:
    print(f"Area: {shape.area():.2f}, Perimeter: {shape.perimeter():.2f}")
    # Each shape calculates area and perimeter differently

Special Methods (Magic Methods)

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def __str__(self):  # String representation for users
        return f"'{self.title}' by {self.author}"
    
    def __repr__(self):  # String representation for developers
        return f"Book('{self.title}', '{self.author}', {self.pages})"
    
    def __len__(self):  # Makes len() work
        return self.pages
    
    def __eq__(self, other):  # Makes == work
        if isinstance(other, Book):
            return self.title == other.title and self.author == other.author
        return False
    
    def __lt__(self, other):  # Makes < work (for sorting)
        return self.pages < other.pages

# Using special methods
book1 = Book("1984", "George Orwell", 328)
book2 = Book("Animal Farm", "George Orwell", 112)

print(str(book1))     # '1984' by George Orwell
print(repr(book1))    # Book('1984', 'George Orwell', 328)
print(len(book1))     # 328
print(book1 == book2) # False
print(book1 > book2)  # True (328 > 112 pages)

books = [book1, book2]
books.sort()  # Uses __lt__ method
print([str(book) for book in books])  # Sorted by pages

Property Decorators

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be 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
    
    @property
    def kelvin(self):
        return self._celsius + 273.15

# Using properties
temp = Temperature(25)
print(f"Celsius: {temp.celsius}")     # 25
print(f"Fahrenheit: {temp.fahrenheit}") # 77.0
print(f"Kelvin: {temp.kelvin}")       # 298.15

temp.fahrenheit = 100  # Sets celsius automatically
print(f"Celsius: {temp.celsius}")     # 37.77...
💡 OOP Principles:
← Functions Next: Error Handling →