Python Classes and Objects

Python Classes and Objects

Classes and objects are the foundation of object-oriented programming in Python. Classes define blueprints for creating objects, which are instances of those classes. This tutorial covers the basics of creating and using classes.

What are Classes and Objects?

Classes are templates for creating objects. Objects are instances of classes with their own data and behavior.

Basic Class Definition

# Define a simple class
class Dog:
    """A simple Dog class."""
    
    # Class attribute (shared by all instances)
    species = "Canis familiaris"
    
    # Constructor method
    def __init__(self, name, age):
        # Instance attributes (unique to each instance)
        self.name = name
        self.age = age
    
    # Instance method
    def bark(self):
        return f"{self.name} says woof!"

# Create objects (instances)
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

print(dog1.bark())  # Buddy says woof!
print(dog2.bark())  # Max says woof!

Classes use the class keyword. The __init__ method initializes new objects. See class definition in the tutorial.

Instance Attributes and Methods

Instance attributes store data unique to each object. Instance methods define object behavior.

The self Parameter

class Person:
    def __init__(self, name, age):
        self.name = name  # Instance attribute
        self.age = age    # Instance attribute
    
    def introduce(self):
        return f"Hi, I'm {self.name} and I'm {self.age} years old."
    
    def have_birthday(self):
        self.age += 1
        return f"Happy birthday! Now I'm {self.age}."

# Create and use person
person = Person("Alice", 30)
print(person.introduce())
person.have_birthday()
print(person.introduce())

self refers to the current instance. It’s required for instance methods. Learn about instance methods.

Class Attributes

Class attributes are shared by all instances of the class.

class Car:
    # Class attributes
    wheels = 4
    engine_type = "gasoline"
    
    def __init__(self, make, model, year):
        self.make = make      # Instance attribute
        self.model = model    # Instance attribute
        self.year = year      # Instance attribute

# All cars share these attributes
car1 = Car("Toyota", "Camry", 2020)
car2 = Car("Honda", "Civic", 2021)

print(f"All cars have {Car.wheels} wheels")
print(f"Car1: {car1.make} {car1.model}")
print(f"Car2: {car2.make} {car2.model}")

Class attributes are accessed via the class name. See class and instance attributes.

Methods

Methods define what objects can do. There are instance, class, and static methods.

Instance Methods

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
    
    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):
        if 0 < amount <= self.balance:
            self.balance -= amount
            return f"Withdrew ${amount}. New balance: ${self.balance}"
        return "Invalid withdrawal amount"
    
    def get_balance(self):
        return f"Account balance: ${self.balance}"

# Use the account
account = BankAccount("Alice", 1000)
print(account.deposit(500))
print(account.withdraw(200))
print(account.get_balance())

Instance methods work with individual object data.

Class Methods

Class methods work with class-level data and are called on the class.

class Student:
    # Class attribute to track all students
    all_students = []
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        # Add to class list
        Student.all_students.append(self)
    
    @classmethod
    def get_total_students(cls):
        return len(cls.all_students)
    
    @classmethod
    def create_from_string(cls, student_string):
        name, grade = student_string.split(",")
        return cls(name.strip(), grade.strip())

# Create students
student1 = Student("Alice", "A")
student2 = Student("Bob", "B")
student3 = Student.create_from_string("Charlie, A")

print(f"Total students: {Student.get_total_students()}")

Class methods use @classmethod decorator and cls parameter. See class methods.

Static Methods

Static methods don’t access instance or class data.

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b
    
    @staticmethod
    def is_even(number):
        return number % 2 == 0
    
    @staticmethod
    def factorial(n):
        if n <= 1:
            return 1
        return n * MathUtils.factorial(n - 1)

# Use static methods
result = MathUtils.add(5, 3)
even = MathUtils.is_even(4)
fact = MathUtils.factorial(5)

print(f"5 + 3 = {result}")
print(f"4 is even: {even}")
print(f"5! = {fact}")

Static methods use @staticmethod decorator. They’re utility functions in classes. See static methods.

Properties

Properties allow controlled access to attributes.

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 can't 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

# Use properties
temp = Temperature(20)
print(f"Celsius: {temp.celsius}°C")
print(f"Fahrenheit: {temp.fahrenheit}°F")

temp.fahrenheit = 68
print(f"New Celsius: {temp.celsius}°C")

Properties use @property decorator. They provide getter/setter functionality. Learn about properties.

Inheritance

Classes can inherit from other classes, gaining their attributes and methods.

Basic Inheritance

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Some sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

# Create instances
dog = Dog("Buddy")
cat = Cat("Whiskers")

print(f"{dog.name} says: {dog.speak()}")
print(f"{cat.name} says: {cat.speak()}")

Child classes inherit from parent classes. The speak method is overridden. See inheritance.

super() Function

Use super() to call parent class methods.

class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def get_info(self):
        return f"{self.make} {self.model}"

class ElectricVehicle(Vehicle):
    def __init__(self, make, model, battery_range):
        super().__init__(make, model)  # Call parent constructor
        self.battery_range = battery_range
    
    def get_info(self):
        base_info = super().get_info()  # Call parent method
        return f"{base_info} - {self.battery_range} miles range"

# Use inheritance
tesla = ElectricVehicle("Tesla", "Model 3", 358)
print(tesla.get_info())

super() accesses parent class methods. It’s essential for inheritance. See super().

Special Methods

Special methods customize object behavior.

String Representation

class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year
    
    def __str__(self):
        return f"{self.title} by {self.author} ({self.year})"
    
    def __repr__(self):
        return f"Book('{self.title}', '{self.author}', {self.year})"

book = Book("1984", "George Orwell", 1949)
print(str(book))   # User-friendly string
print(repr(book))  # Developer-friendly string

__str__ for user display, __repr__ for debugging. See special methods.

Comparison Methods

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __eq__(self, other):
        return isinstance(other, Person) and self.age == other.age
    
    def __lt__(self, other):
        return isinstance(other, Person) and self.age < other.age
    
    def __le__(self, other):
        return self < other or self == other

person1 = Person("Alice", 30)
person2 = Person("Bob", 25)
person3 = Person("Charlie", 30)

print(person1 == person3)  # True (same age)
print(person1 > person2)   # True (older)

Comparison operators can be customized. Learn about rich comparisons.

Class Design Principles

Encapsulation

Hide internal details and provide a clean interface.

class EmailService:
    def __init__(self, smtp_server):
        self._smtp_server = smtp_server  # Private attribute
        self._connection = None
    
    def _connect(self):  # Private method
        # Internal connection logic
        pass
    
    def send_email(self, to, subject, body):  # Public interface
        self._connect()
        # Send email logic
        return f"Email sent to {to}"

service = EmailService("smtp.example.com")
service.send_email("user@example.com", "Hello", "Message body")
# service._connect()  # Should not call private methods directly

Use underscore prefix for private members. See encapsulation.

Composition over Inheritance

Favor composition when possible.

class Engine:
    def start(self):
        return "Engine started"

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

car = Car("Toyota", "Camry")
print(car.start())

Composition is often more flexible than inheritance. See composition examples.

Common Patterns

Factory Pattern

class Shape:
    @staticmethod
    def create_circle(radius):
        return Circle(radius)
    
    @staticmethod
    def create_square(side):
        return Square(side)

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

class Square:
    def __init__(self, side):
        self.side = side
    
    def area(self):
        return self.side ** 2

# Use factory
circle = Shape.create_circle(5)
square = Shape.create_square(4)

print(f"Circle area: {circle.area()}")
print(f"Square area: {square.area()}")

Factory methods create objects without specifying exact classes.

Best Practices

  1. Use descriptive class and method names
  2. Keep classes focused on single responsibilities
  3. Use properties for attribute access control
  4. Prefer composition over complex inheritance
  5. Document classes and methods with docstrings

External Resources:

Related Tutorials:

Last updated on