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).
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:
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).
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.
# 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()
).
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
).
# 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).
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.
# 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
# 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
andwithdraw
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
andaccount2
are created (instantiated) and methods are called on them. Each object maintains its own balance.
Common Mistakes or Pitfalls
Forgetting self:
Forgetting to includeself
as the first parameter in instance method definitions (def my_method():
instead ofdef my_method(self):
). This leads toTypeError
.Forgetting to use self:
Inside methods, forgetting to useself.
to access instance attributes or other instance methods (attribute_name
instead ofself.attribute_name
). This leads toNameError
.- 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 andself.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, useClassName.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.
- Instance methods operate on objects; their first parameter is
Exercises & Mini Projects
Exercises
Define Person Class:
Define a class namedPerson
with an__init__
method that takesname
andage
as arguments and assigns them to instance attributesself.name
andself.age
.Instantiate Person:
Create twoPerson
objects with different names and ages. Print thename
andage
attributes of each object.Add introduce Method:
Add an instance method calledintroduce(self)
to thePerson
class. This method should print a string like “Hi, my name is [Name] and I am [Age] years old.” Call this method on bothPerson
objects you created.- Class Attribute: Add a class attribute
species = "Homo sapiens"
to thePerson
class. Modify theintroduce
method to include the species in its output. Print the species using both an instance (person_object.species
) and the class name (Person.species
). Simple Circle Class:
Define a classCircle
with an__init__
method that takes aradius
and stores it as an instance attribute. Add two methods:calculate_area(self)
andcalculate_circumference(self)
that return the area and circumference, respectively (you can use3.14159
for pi or importmath.pi
). Create aCircle
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:
Define the Book Class:
- Create a class named
Book
. - Define the
__init__
method. It should accepttitle
,author
, andisbn
(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 toFalse
within__init__
.
- Create a class named
- 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
isFalse
. - If it’s
False
, setself.is_checked_out
toTrue
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.”
- Checks if
- Define an instance method
check_in(self)
that:- Checks if
self.is_checked_out
isTrue
. - If it’s
True
, setself.is_checked_out
toFalse
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.”
- Checks if
- Define an instance method
- 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).
- Create at least two different
dditional Sources: