Virtual assistance

Python Classes and Objects

Classes and objects are the foundation of object-oriented programming in Python. Learn how to define classes, create objects, and work with methods and attributes.

Python Classes and Objects

What are Classes and Objects?

A class is a blueprint for creating objects. An object is an instance of a class. Classes define the structure and behavior that objects of that class will have.

"Classes are templates for objects, defining their attributes and methods."

Defining Classes

Use the class keyword to define a class:

# Basic class definition
class Person:
    pass

# Creating an object (instance)
person1 = Person()
print(type(person1))  # <class '__main__.Person'>

# Class with attributes
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

# Creating objects with attributes
car1 = Car("Toyota", "Camry", 2020)
car2 = Car("Honda", "Civic", 2019)

print(f"{car1.make} {car1.model} ({car1.year})")
print(f"{car2.make} {car2.model} ({car2.year})")

The __init__ Method

The __init__ method initializes object attributes:

class Student:
    def __init__(self, name, age, grade):
        self.name = name
        self.age = age
        self.grade = grade
    
    def display_info(self):
        print(f"Name: {self.name}")
        print(f"Age: {self.age}")
        print(f"Grade: {self.grade}")

# Creating student objects
student1 = Student("Alice", 16, "10th")
student2 = Student("Bob", 17, "11th")

student1.display_info()
print()
student2.display_info()

Instance Methods

Methods define the behavior of objects:

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
    
    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            print(f"Deposited ${amount}. New balance: ${self.balance}")
        else:
            print("Invalid deposit amount")
    
    def withdraw(self, amount):
        if amount > 0 and amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.balance}")
        else:
            print("Invalid withdrawal amount")
    
    def get_balance(self):
        return self.balance

# Using the bank account
account = BankAccount("John Doe", 1000)
account.deposit(500)
account.withdraw(200)
print(f"Final balance: ${account.get_balance()}")

Class Attributes vs Instance Attributes

class Circle:
    # Class attribute (shared by all instances)
    pi = 3.14159
    
    def __init__(self, radius):
        # Instance attribute (unique to each instance)
        self.radius = radius
    
    def area(self):
        return self.pi * self.radius ** 2
    
    def circumference(self):
        return 2 * self.pi * self.radius

# All circles share the same pi value
circle1 = Circle(5)
circle2 = Circle(10)

print(f"Circle 1 area: {circle1.area()}")        # 78.53975
print(f"Circle 2 area: {circle2.area()}")        # 314.159

# Changing class attribute affects all instances
Circle.pi = 3.14
print(f"Circle 1 area (new pi): {circle1.area()}")  # 78.5

# Instance attributes are unique
circle1.radius = 7
print(f"Circle 1 new area: {circle1.area()}")     # 153.86
print(f"Circle 2 area unchanged: {circle2.area()}") # 314.159

Special Methods (Dunder Methods)

Special methods customize object behavior:

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def __str__(self):
        return f"'{self.title}' by {self.author}"
    
    def __len__(self):
        return self.pages
    
    def __eq__(self, other):
        return self.title == other.title and self.author == other.author
    
    def __add__(self, other):
        return self.pages + other.pages

book1 = Book("1984", "George Orwell", 328)
book2 = Book("Animal Farm", "George Orwell", 112)

print(book1)           # '1984' by George Orwell
print(len(book1))      # 328
print(book1 == book2)  # False
print(book1 + book2)   # 440

Property Decorators

Use properties for controlled attribute access:

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

temp = Temperature(25)
print(f"Celsius: {temp.celsius}")      # 25
print(f"Fahrenheit: {temp.fahrenheit}") # 77.0

temp.fahrenheit = 100
print(f"Celsius: {temp.celsius}")      # 37.777...
print(f"Fahrenheit: {temp.fahrenheit}") # 100.0

Class Methods and Static Methods

class MathUtils:
    @staticmethod
    def is_even(number):
        return number % 2 == 0
    
    @staticmethod
    def factorial(n):
        if n == 0:
            return 1
        return n * MathUtils.factorial(n - 1)
    
    @classmethod
    def create_from_string(cls, math_expr):
        # Parse and create object
        return cls()

# Static methods don't need instance
print(MathUtils.is_even(4))     # True
print(MathUtils.factorial(5))   # 120

# Class methods work with class
obj = MathUtils.create_from_string("2+2")

Inheritance Basics

Classes can inherit from other classes:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        pass
    
    def eat(self):
        print(f"{self.name} is eating")

class Dog(Animal):
    def speak(self):
        print(f"{self.name} says Woof!")

class Cat(Animal):
    def speak(self):
        print(f"{self.name} says Meow!")

dog = Dog("Buddy")
cat = Cat("Whiskers")

dog.speak()   # Buddy says Woof!
cat.speak()   # Whiskers says Meow!
dog.eat()     # Buddy is eating
cat.eat()     # Whiskers is eating

Object Composition

Objects can contain other objects:

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower
    
    def start(self):
        print("Engine started")

class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.engine = Engine(200)  # Composition
    
    def start(self):
        print(f"Starting {self.make} {self.model}")
        self.engine.start()

my_car = Car("Toyota", "Camry")
my_car.start()

Best Practices

  • Use descriptive class names: Start with capital letters
  • Use self consistently: For instance references
  • Initialize all attributes in __init__: Avoid surprises
  • Use properties for validation: Control attribute access
  • Keep methods small: Single responsibility principle
  • Use inheritance judiciously: Favor composition when possible
  • Document classes and methods: Use docstrings
  • Use meaningful attribute names: Avoid abbreviations

Common Patterns

# Data class pattern
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def distance_from_origin(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    
    def __str__(self):
        return f"Point({self.x}, {self.y})"

# Factory pattern
class ShapeFactory:
    @staticmethod
    def create_circle(radius):
        return Circle(radius)
    
    @staticmethod
    def create_rectangle(width, height):
        return Rectangle(width, height)

# Singleton pattern (basic implementation)
class DatabaseConnection:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Builder pattern
class Pizza:
    def __init__(self):
        self.toppings = []
    
    def add_topping(self, topping):
        self.toppings.append(topping)
        return self  # Enable method chaining
    
    def build(self):
        return f"Pizza with {', '.join(self.toppings)}"

pizza = Pizza().add_topping("cheese").add_topping("pepperoni").build()
print(pizza)  # Pizza with cheese, pepperoni

Classes and objects are fundamental to object-oriented programming in Python. They allow you to model real-world entities and organize code in a logical, reusable way.