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.
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.