Programming with Python | Chapter 12: Object-Oriented Programming (OOP) – Classes and Objects

Chapter Objectives

  • Understand the basic principles of Object-Oriented Programming (OOP).
  • Differentiate between procedural programming and OOP.
  • Define classes as blueprints for creating objects.
  • Understand objects (instances) as concrete realizations of classes.
  • Learn how to define a class using the class keyword.
  • Define attributes (data associated with an object or class).
    • Understand instance attributes (unique to each object).
    • Understand class attributes (shared by all instances of a class).
  • Define methods (functions associated with a class) to represent object behavior.
  • Understand the special __init__ method (constructor) for initializing objects.
  • Understand the role of the self parameter in instance methods.
  • Learn how to create (instantiate) objects from a class.

Introduction

So far, we’ve primarily used procedural programming: organizing code into functions that operate on data. Object-Oriented Programming (OOP) offers a different way to structure programs by bundling data and the functions that operate on that data together into units called objects. This chapter introduces the core concepts of OOP in Python: classes and objects. A class acts as a blueprint or template, defining the properties (attributes) and behaviors (methods) that all objects of a certain type will share. An object is a specific instance created from that class blueprint. OOP helps create modular, reusable, and often more intuitive code, especially for complex systems that model real-world entities.

Theory & Explanation

Procedural vs. Object-Oriented Programming

  • Procedural: Focuses on procedures (functions) that perform operations on data. Data and functions are typically separate. Think of writing a series of instructions or recipes. (e.g., C, early Python scripts).
  • Object-Oriented: Focuses on “objects” which combine data (attributes) and behavior (methods) into a single unit. Programs are designed as interactions between these objects. Think of modeling real-world things and their interactions. (e.g., Java, C++, Python).

OOP aims to manage complexity by modeling concepts as objects, leading to benefits like:

  • Encapsulation: Bundling data and methods within an object, potentially hiding internal details.
  • Inheritance: Creating new classes that reuse and extend properties from existing classes (covered later).
  • Polymorphism: Allowing objects of different classes to respond to the same method call in different ways (covered later).
graph LR
    subgraph proc["Procedural Programming"]
        direction LR
        P_Data["Data Structures"] --> P_Func["Functions Operate on Data"]
    end

    subgraph oop["Object-Oriented Programming (OOP)"]
        direction LR
        O_Data["Attributes / Data"] --> O_Obj["Objects<br/>(Data + Behavior)"]
        O_Behav["Methods / Behavior"] --> O_Obj
        O_Obj --> O_Int["Objects Interact"]
    end

    P_Func --- P_Data

    style proc fill:#E0F2FE,stroke:#0284C7,stroke-width:1px
    style oop fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px

Classes and Objects: The Blueprint and the Instance

  • Class: A blueprint, template, or definition for creating objects. It defines a set of attributes (variables) that objects of the class will have and methods (functions) that objects of the class can perform. The class itself doesn’t do anything; it just defines the structure and behavior.
    • Analogy: The architectural blueprint for a house.
  • Object (Instance): A specific realization created from a class. Each object has its own set of attribute values (its state) but shares the methods defined by the class. Multiple objects can be created from the same class.
    • Analogy: An actual house built according to the blueprint. You can build many houses (objects) from the same blueprint (class).
Class: Dog Attributes: – name – breed Methods: – describe() Object: dog1 name: “Buddy” breed: “GoldenDoodle” Object: dog2 name: “Lucy” breed: “Labrador” Object: dog3 … creates

Defining a Class (class)

You define a class using the class keyword, followed by the class name (conventionally in CamelCase or PascalCase), and a colon :. The indented block contains the class definition, including attributes and methods.

Syntax:

Python
class ClassName:
    """Optional docstring describing the class."""

    # Class attribute (shared by all instances)
    class_attribute = value

    # Initializer method (constructor)
    def __init__(self, parameter1, parameter2, ...):
        """Initializes a new instance of the class."""
        # Instance attributes (unique to each instance)
        self.instance_attribute1 = parameter1
        self.instance_attribute2 = value_or_calculation
        # ...

    # Instance method
    def method_name(self, parameter1, ...):
        """Performs some action related to the object."""
        # Code block using self.instance_attribute, parameters, etc.
        # ...
        return result # Optional

graph LR
    Class["Dog Class"]

    subgraph ClassDef ["Blueprint: class Dog"]
        direction LR
        A["Attributes:<br/>- species (Class)<br/>- name (Instance)<br/>- breed (Instance)"]
        M["Methods:<br/>- __init__(self, name, breed)<br/>- describe(self)<br/>- add_trick(self, trick)"]
    end

    subgraph Instances ["Objects"]
        direction TB
        O1["Object: dog1<br/>name: 'Buddy'<br/>breed: 'GoldenDoodle'"]
        O2["Object: dog2<br/>name: 'Lucy'<br/>breed: 'Labrador'"]
        O3["Object: dog3<br/>name: 'Max'<br/>breed: 'Poodle'"]
    end

    Class --> O1
    Class --> O2
    Class --> O3

    ClassDef --> O1
    ClassDef --> O2
    ClassDef --> O3

    style ClassDef fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px
    style Instances fill:#F3F4F6,stroke:#9CA3AF,stroke-width:1px

Attributes: Storing Data

Attributes are variables associated with a class or an object.

Class Attributes: Defined directly inside the class block but outside any method. They are shared by all instances (objects) of the class. If you change a class attribute, the change reflects in all instances (unless an instance has overridden it with its own attribute of the same name).

Python
class Dog:
    species = "Canis familiaris" # Class attribute

    def __init__(self, name):
        self.name = name # Instance attribute

dog1 = Dog("Buddy")
dog2 = Dog("Lucy")

print(dog1.species) # Output: Canis familiaris
print(dog2.species) # Output: Canis familiaris
print(Dog.species)  # Can also access via the class name

Instance Attributes: Defined inside methods, typically within the __init__ method, using self.attribute_name = value. Each object created from the class gets its own copy of instance attributes. Changes to an instance attribute only affect that specific object.

Python
# Continuing the Dog example above:
print(dog1.name) # Output: Buddy
print(dog2.name) # Output: Lucy

dog1.name = "Buddy Boy" # Change instance attribute for dog1 only
print(dog1.name) # Output: Buddy Boy
print(dog2.name) # Output: Lucy (dog2 is unaffected)

Methods: Defining Behavior

Methods are functions defined inside a class. They operate on the data (attributes) associated with an object or the class itself.

Instance Methods:

The most common type. They operate on a specific instance (object) of the class. Their first parameter is always self (by convention, though technically any name works), which refers to the instance calling the method. Python passes the instance automatically when you call the method on an object (my_object.method()).

Python
class Dog:
    species = "Canis familiaris"

    def __init__(self, name, breed):
        self.name = name
        self.breed = breed
        self.tricks = [] # Each dog gets its own list of tricks

    # Instance method (takes 'self' as first parameter)
    def describe(self):
        """Returns a description of the dog."""
        return f"{self.name} is a {self.breed} ({self.species})."

    # Another instance method
    def add_trick(self, trick_name):
        """Adds a trick to the dog's list."""
        self.tricks.append(trick_name)
        print(f"{self.name} learned {trick_name}!")

# Create an instance
my_dog = Dog("Fido", "Golden Retriever")

# Call instance methods on the object
description = my_dog.describe()
print(description) # Output: Fido is a Golden Retriever (Canis familiaris).

my_dog.add_trick("sit")   # Output: Fido learned sit!
my_dog.add_trick("fetch") # Output: Fido learned fetch!
print(f"{my_dog.name}'s tricks: {my_dog.tricks}") # Output: Fido's tricks: ['sit', 'fetch']

Notice how describe and add_trick use self to access the specific object’s name, breed, and tricks.

The __init__ Method (Constructor/Initializer):

This is a special dunder (double underscore) method. It’s automatically called when you create a new object (instance) from the class (ClassName(...)). Its primary purpose is to initialize the instance attributes of the object. The self parameter refers to the newly created object. Any arguments passed during object creation (after the class name) are passed to __init__ (after self).

Python
# In the Dog class definition above:
# def __init__(self, name, breed):
#     self.name = name # Initializes the 'name' attribute for the specific dog object
#     self.breed = breed # Initializes the 'breed' attribute

# When we call:
# my_dog = Dog("Fido", "Golden Retriever")
# Python internally does something like:
# 1. Creates an empty Dog object.
# 2. Calls Dog.__init__(<the new object>, "Fido", "Golden Retriever")
# 3. The __init__ method assigns "Fido" to the object's 'name' and "Golden Retriever" to its 'breed'.
# 4. The newly initialized object is assigned to my_dog.

The self Parameter:

self represents the instance of the class on which a method is being called. It allows methods to access and modify the object’s attributes (self.attribute) and call other methods of the object (self.other_method()). It’s always the first parameter in the definition of an instance method, but you don’t provide it explicitly when calling the method on an object (my_object.method(arg1) – Python passes the object my_object as self automatically).

Class Scope (e.g., Dog) species = “Canis familiaris” (Shared by all instances) Object: dog1 name = “Buddy” breed = “GoldenDoodle” inherits/accesses Object: dog2 name = “Lucy” breed = “Labrador” inherits/accesses

Creating Objects (Instantiation)

Creating an object from a class is called instantiation. You do this by calling the class name as if it were a function, passing any arguments required by the __init__ method.

Python
# Syntax: variable_name = ClassName(arguments_for_init)

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_description(self):
        return f"{self.year} {self.make} {self.model}"

    def read_odometer(self):
        print(f"This car has {self.odometer_reading} miles on it.")

# Instantiate (create) two Car objects
my_car = Car("Toyota", "Camry", 2022)
your_car = Car("Ford", "Mustang", 2023)

# Access attributes and call methods on the specific objects
print(my_car.get_description()) # Output: 2022 Toyota Camry
my_car.read_odometer()          # Output: This car has 0 miles on it.

print(your_car.get_description()) # Output: 2023 Ford Mustang
your_car.odometer_reading = 50 # Can modify instance attributes directly (if needed)
your_car.read_odometer()          # Output: This car has 50 miles on it.

Code Examples

Example 1: Simple BankAccount Class

Python
# bank_account.py

class BankAccount:
    """Represents a simple bank account."""

    # Class attribute (e.g., interest rate - shared, though maybe unrealistic)
    interest_rate = 0.01

    def __init__(self, account_holder, initial_balance=0.0):
        """Initializes a new bank account."""
        # Instance attributes
        self.account_holder = account_holder
        # Use a leading underscore for 'internal' attributes (convention)
        self._balance = float(initial_balance)

    def deposit(self, amount):
        """Deposits funds into the account."""
        if amount > 0:
            self._balance += amount
            print(f"Deposited ${amount:.2f}. New balance: ${self._balance:.2f}")
        else:
            print("Deposit amount must be positive.")

    def withdraw(self, amount):
        """Withdraws funds from the account if sufficient balance exists."""
        if amount <= 0:
            print("Withdrawal amount must be positive.")
        elif amount > self._balance:
            print(f"Insufficient funds. Cannot withdraw ${amount:.2f}.")
        else:
            self._balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self._balance:.2f}")

    def get_balance(self):
        """Returns the current account balance."""
        return self._balance

    def display_details(self):
        """Prints the account details."""
        print(f"\n--- Account Details ---")
        print(f"Holder: {self.account_holder}")
        print(f"Balance: ${self.get_balance():.2f}") # Call another method using self
        print(f"Interest Rate: {BankAccount.interest_rate:.2%}") # Access class attribute
        print(f"-----------------------")


# --- Using the BankAccount class ---
account1 = BankAccount("Alice Smith", 1000.0)
account2 = BankAccount("Bob Johnson") # Uses default initial_balance=0.0

account1.display_details()
account2.display_details()

account1.deposit(500.50)
account1.withdraw(200)
account1.withdraw(2000) # Insufficient funds

account2.deposit(150)
account2.withdraw(20)

account1.display_details()
account2.display_details()

# Accessing balance directly (possible, but using get_balance is preferred)
# print(f"Alice's balance directly: {account1._balance}")

Explanation:

  • The BankAccount class defines attributes (interest_rate – class, account_holder, _balance – instance) and methods (__init__, deposit, withdraw, get_balance, display_details).
  • __init__ sets up the initial state of each account object. Note the use of a leading underscore _balance – this is a convention indicating the attribute is intended for internal use (though Python doesn’t strictly enforce privacy).
  • Methods like deposit and withdraw modify the object’s state (self._balance).
  • display_details accesses both instance attributes (self.account_holder) and class attributes (BankAccount.interest_rate) and calls another instance method (self.get_balance()).
  • Objects account1 and account2 are created (instantiated) and methods are called on them. Each object maintains its own balance.

Common Mistakes or Pitfalls

  • Forgetting self: Forgetting to include self as the first parameter in instance method definitions (def my_method(): instead of def my_method(self):). This leads to TypeError.
  • Forgetting to use self: Inside methods, forgetting to use self. to access instance attributes or other instance methods (attribute_name instead of self.attribute_name). This leads to NameError.
  • Confusing Class and Instance Attributes: Incorrectly accessing or modifying class attributes when instance attributes were intended, or vice-versa. Remember ClassName.attribute for class attributes and self.attribute for instance attributes within methods.
  • Incorrect __init__: Misspelling __init__ (e.g., __int__) means the initializer won’t be called automatically during object creation.
  • Calling __init__ Directly: You generally don’t call __init__ directly; it’s called automatically during instantiation (ClassName(...)).
  • Modifying Class Attributes Incorrectly: Assigning to self.class_attribute = value creates an instance attribute that shadows the class attribute for that specific instance, rather than changing the shared class attribute. To change the class attribute for all instances, use ClassName.class_attribute = value.

Chapter Summary

Concept Syntax / Example Description
Class class MyClass: ... A blueprint or template for creating objects. Defines attributes and methods. Uses CamelCase naming convention.
Object (Instance) my_obj = MyClass() A specific realization created from a class. Holds its own state (attribute values) but shares class methods.
Attribute self.attr / ClassName.attr Variables associated with a class or object, storing data.
Instance Attribute self.name = "value" (inside __init__ or methods) Data unique to each specific object (instance). Defined using self.
Class Attribute species = "..." (inside class, outside methods) Data shared by all instances of the class. Accessed via ClassName.attribute or self.attribute (if not shadowed by an instance attribute).
Method def my_method(self, ...): ... Functions defined inside a class that define the behavior of objects.
Instance Method def action(self, param): ... Methods that operate on a specific instance. The first parameter is always self, referring to the instance.
__init__ Method def __init__(self, p1, p2): ... Special “initializer” or “constructor” method. Automatically called when an object is created (instantiated) to set up its initial state (instance attributes).
self Parameter self (as first parameter of instance methods) A reference to the specific instance on which a method is being called. Used to access instance attributes (self.attr) and other instance methods (self.method()). Passed automatically by Python.
Instantiation my_car = Car("Toyota", "Camry") The process of creating an object (instance) from a class by calling the class name like a function. Arguments are passed to __init__.
  • Object-Oriented Programming (OOP) models problems using objects that bundle data (attributes) and behavior (methods).
  • A class is a blueprint defined using the class keyword.
  • An object (or instance) is created from a class (my_obj = ClassName()).
  • Attributes store data:
    • Class attributes are shared by all instances.
    • Instance attributes are unique to each object, typically initialized in __init__.
  • Methods are functions defined within a class:
    • Instance methods operate on objects; their first parameter is self.
    • __init__(self, ...) is the special initializer method called automatically when an object is created.
    • self refers to the specific instance the method is called on.

Exercises & Mini Projects

Exercises

  1. Define Person Class: Define a class named Person with an __init__ method that takes name and age as arguments and assigns them to instance attributes self.name and self.age.
  2. Instantiate Person: Create two Person objects with different names and ages. Print the name and age attributes of each object.
  3. Add introduce Method: Add an instance method called introduce(self) to the Person class. This method should print a string like “Hi, my name is [Name] and I am [Age] years old.” Call this method on both Person objects you created.
  4. Class Attribute: Add a class attribute species = "Homo sapiens" to the Person class. Modify the introduce method to include the species in its output. Print the species using both an instance (person_object.species) and the class name (Person.species).
  5. Simple Circle Class: Define a class Circle with an __init__ method that takes a radius and stores it as an instance attribute. Add two methods: calculate_area(self) and calculate_circumference(self) that return the area and circumference, respectively (you can use 3.14159 for pi or import math.pi). Create a Circle object and print its area and circumference.

Mini Project: Simple Book Class

Goal: Create a class to represent books in a small library or bookstore context.

Steps:

  1. Define the Book Class:
    • Create a class named Book.
    • Define the __init__ method. It should accept title, author, and isbn (International Standard Book Number) as arguments and store them as instance attributes (self.title, self.author, self.isbn).
    • Add another instance attribute is_checked_out and initialize it to False within __init__.
  2. Add Methods:
    • Define an instance method display_info(self) that prints the book’s title, author, and ISBN in a readable format.
    • Define an instance method check_out(self) that:
      • Checks if self.is_checked_out is False.
      • If it’s False, set self.is_checked_out to True and print a confirmation message like “[Title] has been checked out.”
      • If it’s already True, print a message like “[Title] is already checked out.”
    • Define an instance method check_in(self) that:
      • Checks if self.is_checked_out is True.
      • If it’s True, set self.is_checked_out to False and print a confirmation message like “[Title] has been checked in.”
      • If it’s already False, print a message like “[Title] was not checked out.”
  3. Instantiate and Use:
    • Create at least two different Book objects with sample data.
    • Call display_info() on both books.
    • Try checking out the first book using check_out().
    • Try checking out the first book again (it should say it’s already checked out).
    • Call display_info() on the first book again (to potentially see if status changed, though we aren’t printing it here).
    • Check in the first book using check_in().
    • Try checking in the second book (which wasn’t checked out).

dditional Sources:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top