# Creating and managing cart state with functions - gets messy fast!
def create_cart():
return {"items": [], "subtotal": 0, "tax_rate": 0.08, "discount": 0}
def add_item(cart, name, price, quantity=1):
cart["items"].append({"name": name, "price": price, "qty": quantity})
cart["subtotal"] += price * quantity
return cart
def apply_discount(cart, discount_percent):
cart["discount"] = discount_percent
return cart
def calculate_total(cart):
discounted = cart["subtotal"] * (1 - cart["discount"]/100)
return discounted * (1 + cart["tax_rate"])
# Usage becomes cumbersome and error-prone
cart = create_cart()
cart = add_item(cart, "Laptop", 999.99)
cart = add_item(cart, "Mouse", 29.99)
cart = apply_discount(cart, 10)
total = calculate_total(cart)
print(f"Total: ${total:.2f}")IS 4010: Application Development with Artificial Intelligence
Week 06: Object-oriented programming
Brandon M. Greenwell
Object-oriented programming
Session 1: from functions to objects
When functions aren’t enough: a shopping cart story
The problem: managing complex state with functions
- Scenario: Building an e-commerce shopping cart system
- Challenge: Multiple pieces of related data (items, totals, discounts, tax rates)
- Function approach: Becomes unwieldy as complexity grows
- Design pressure: Related state may be passed repeatedly, updated inconsistently, or duplicated
A function-based shopping cart
Your turn: feel the pain
Try to add more functionality to the function-based cart: 1. Add a function to remove items 2. Add a function to change item quantities 3. Notice how many parameters you need to pass around!
# Your code here - try adding remove_item() and change_quantity() functions
# Notice how complex the parameter passing becomes!
def remove_item(cart, name):
"""Remove an item from the cart."""
for item in cart["items"]:
if item["name"] == name:
cart["subtotal"] -= item["price"] * item["qty"]
cart["items"].remove(item)
break
return cart
def change_quantity(cart, name, quantity):
"""Change the quantity of an item in the cart."""
for item in cart["items"]:
if item["name"] == name:
cart["subtotal"] -= item["price"] * item["qty"]
item["qty"] = quantity
cart["subtotal"] += item["price"] * item["qty"]
break
return cartA class-based shopping cart
class ShoppingCart:
"""Represents a shopping cart with items, discounts, and tax calculation."""
def __init__(self, tax_rate=0.08):
self.items = []
self.tax_rate = tax_rate
self.discount_percent = 0
def add_item(self, name, price, quantity=1):
"""Add an item to the cart."""
self.items.append({"name": name, "price": price, "qty": quantity})
def apply_discount(self, discount_percent):
"""Apply a discount to the entire cart."""
self.discount_percent = discount_percent
@property
def subtotal(self):
"""Calculate subtotal before discount and tax."""
return sum(item["price"] * item["qty"] for item in self.items)
@property
def total(self):
"""Calculate final total after discount and tax."""
discounted = self.subtotal * (1 - self.discount_percent/100)
return discounted * (1 + self.tax_rate)
# Usage is clean, intuitive, and maintainable
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
cart.apply_discount(10)
print(f"Total: ${cart.total:.2f}")Your turn: extend the class
Add methods to the ShoppingCart class: 1. remove_item(name) - remove an item by name 2. clear() - empty the cart 3. item_count - property that returns total number of items
class ShoppingCart:
"""Represents a shopping cart with items, discounts, and tax calculation."""
def __init__(self, tax_rate=0.08):
"""Initialize a new shopping cart."""
self.items = []
self.tax_rate = tax_rate
self.discount_percent = 0
def add_item(self, name, price, quantity=1):
"""Add an item to the cart."""
self.items.append({"name": name, "price": price, "qty": quantity})
def remove_item(self, name):
"""Remove an item from the cart by name."""
# Keep only items that don't match the name
self.items = [item for item in self.items if item["name"] != name]
def clear(self):
"""Empty the cart."""
self.items = []
def apply_discount(self, discount_percent):
"""Apply a discount to the entire cart."""
self.discount_percent = discount_percent
@property
def item_count(self):
"""Return total number of items in the cart."""
return sum(item["qty"] for item in self.items)
@property
def subtotal(self):
"""Calculate subtotal before discount and tax."""
return sum(item["price"] * item["qty"] for item in self.items)
@property
def total(self):
"""Calculate final total after discount and tax."""
discounted = self.subtotal * (1 - self.discount_percent/100)
return discounted * (1 + self.tax_rate)
def __str__(self):
"""Return a string representation of the cart."""
lines = ["Shopping Cart:"]
if not self.items:
lines.append(" (Empty)")
else:
for item in self.items:
lines.append(f" {item['name']} x{item['qty']} - ${item['price']:.2f} each")
lines.append(f" Subtotal: ${self.subtotal:.2f}")
if self.discount_percent > 0:
lines.append(f" Discount: {self.discount_percent}%")
lines.append(f" Total: ${self.total:.2f}")
lines.append(f" Item Count: {self.item_count}")
return "\n".join(lines)
# Test the enhanced class
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99, 2)
print("--- Initial Cart ---")
print(cart)
cart.remove_item("Mouse")
print("\n--- After removing Mouse ---")
print(cart)
cart.clear()
print("\n--- After clearing ---")
print(cart)When object-oriented programming helps
- Encapsulation: Bundle data and methods together, hide implementation details
- Code reusability: Write once, use everywhere - create multiple cart instances
- Maintainability: Changes to internal implementation don’t break external code
- Collaboration: Team members can work on different classes independently
- Framework examples: Django, Flask, and FastAPI all provide class-based APIs
What is a class?
- A class is a blueprint or template for creating objects
- An object (or instance) is a specific item created from that class blueprint
- Attributes: The data that belongs to an object (like variables)
- Methods: The functions that operate on an object’s data
- Analogy: A
Carclass is the blueprint; your specific Honda Civic is an object instance
The __init__ method: object initialization
- The
__init__method is a special constructor function - Initialization hook: Runs after Python creates a new instance
- Purpose: Initialize the object’s attributes with starting values
- The
selfparameter: References the specific instance being created - Convention: Always the first parameter in instance methods
class BankAccount:
"""Represents a bank account with balance tracking."""
def __init__(self, account_holder: str, initial_balance: float = 0.0):
# Instance attributes - unique to each account
self.account_holder = account_holder
self.balance = initial_balance
self.transaction_history = []
def deposit(self, amount: float):
"""Add money to the account."""
self.balance += amount
self.transaction_history.append(f"Deposited ${amount:.2f}")
# Create specific account instances
alice_account = BankAccount("Alice Johnson", 1000.0)
bob_account = BankAccount("Bob Smith") # Uses default balance of 0.0Your turn: bank account features
Enhance the BankAccount class: 1. Add a get_transaction_history() method 2. Add an account_number attribute (you can use a simple counter) 3. Add a minimum balance requirement
# Your enhanced BankAccount class hereThe __str__ method: human-readable representation
- The
__str__method defines how objects appear when printed - Automatic invocation: Called by
print(),str(), and string formatting - User-friendly: Should return meaningful information for end users
- AI exercise: Ask for a draft, then verify its attributes and exact string output
- Debugging aid: A useful representation makes object state easier to inspect
class User:
"""Represents a user in a social media application."""
def __init__(self, username: str, email: str, join_date: str):
self.username = username
self.email = email
self.join_date = join_date
self.followers = 0
self.is_verified = False
def __str__(self) -> str:
"""Return a user-friendly string representation."""
verification = "✓" if self.is_verified else ""
return f"@{self.username}{verification} ({self.followers} followers) - Joined {self.join_date}"
# Create and display user instances
user1 = User("grace_hopper", "grace@example.com", "2023-01-15")
user2 = User("ada_lovelace", "ada@example.com", "2023-02-20")
user2.is_verified = True
user2.followers = 50000
print(user1) # @grace_hopper (0 followers) - Joined 2023-01-15
print(user2) # @ada_lovelace✓ (50000 followers) - Joined 2023-02-20Introducing Lab 06: part 1
- Deliverable: Create the specified
Bookclass inweek06/lab06.py - AI collaboration: Ask for an explanation or review, then verify every attribute and method against the tests
- Core concepts: Practice
__init__constructors and__str__representations - Modeling: Connect the lab’s fields and methods to the class contract
- Foundation: Prepare for inheritance in part 2
Session 2: methods and inheritance
Giving objects behavior with methods
- Methods are functions defined inside a class that operate on object data
- Instance methods: Most common type, always take
selfas first parameter - Behavior modeling: Define an object’s operations alongside its data
- Encapsulation: Methods can access and modify private object state safely
- State management: Methods ensure object data stays consistent and valid
class GameCharacter:
"""Represents a character in a role-playing game."""
def __init__(self, name: str, health: int = 100):
self.name = name
self.health = health
self.max_health = health
self.experience = 0
self.level = 1
def take_damage(self, damage: int):
"""Reduce character health, ensuring it doesn't go below 0."""
self.health = max(0, self.health - damage)
print(f"{self.name} takes {damage} damage! Health: {self.health}/{self.max_health}")
def heal(self, amount: int):
"""Restore character health, capped at maximum."""
old_health = self.health
self.health = min(self.max_health, self.health + amount)
healed = self.health - old_health
print(f"{self.name} heals for {healed} points! Health: {self.health}/{self.max_health}")
def gain_experience(self, exp: int):
"""Add experience and level up if threshold is reached."""
self.experience += exp
if self.experience >= self.level * 100: # Simple leveling formula
self.level += 1
self.max_health += 20
self.health = self.max_health # Full heal on level up
print(f"{self.name} reached level {self.level}!")
# Create and interact with a character
hero = GameCharacter("Aria the Brave")
hero.take_damage(30)
hero.heal(15)
hero.gain_experience(150)Your turn: game character enhancement
Add these features to the GameCharacter class: 1. A use_potion() method that heals based on inventory 2. A magic_attack(target) method with different damage 3. A get_inventory_value() method that calculates total value
# Your enhanced GameCharacter class hereDocumenting classes and methods
- The rules from Week 05 carry over. A docstring is a string literal in triple quotes, placed as the first statement
- Class docstring: say what the object represents, not how it is built
- Method docstring: same as any function. Say what it returns or what it changes
- Skip the obvious:
__init__rarely needs more than the class docstring already tells the reader - Lab 06 hands you
get_agewith its docstring written. Match that style for the methods you add
class Book:
"""A book in a personal library."""
def __init__(self, title: str, author: str, year: int):
self.title = title
self.author = author
self.year = year
def get_age(self):
"""Return the number of years since publication."""
return date.today().year - self.yearUnderstanding inheritance: reusing behavior
- Inheritance allows classes to inherit attributes and methods from parent classes
- Code reuse: Write common functionality once, share across multiple related classes
- “Is-a” relationships: Child classes are specialized versions of parent classes
- Hierarchical design: Can model genuine subtype relationships
- Design trade-off: Inheritance can reuse behavior, but composition is often simpler
Implementing inheritance: the super() function
- Syntax:
class ChildClass(ParentClass):establishes inheritance relationship super()function: Calls methods from the parent class- Initializer reuse: A child initializer can call the parent initializer with
super()when it needs the parent’s setup - Method override: Child classes can replace parent methods with specialized versions
- Method extension: Or extend parent methods with additional functionality
class Vehicle:
"""Base class for all vehicles."""
def __init__(self, make: str, model: str, year: int):
self.make = make
self.model = model
self.year = year
self.mileage = 0
def start_engine(self):
"""Start the vehicle's engine."""
print(f"The {self.year} {self.make} {self.model} engine starts.")
def drive(self, miles: float):
"""Drive the vehicle and update mileage."""
self.mileage += miles
print(f"Drove {miles} miles. Total mileage: {self.mileage}")
class ElectricCar(Vehicle):
"""Electric vehicle with battery management."""
def __init__(self, make: str, model: str, year: int, battery_capacity: float):
super().__init__(make, model, year) # Call parent constructor
self.battery_capacity = battery_capacity
self.battery_level = 100.0 # Start fully charged
def start_engine(self):
"""Override: Electric cars don't have traditional engines."""
print(f"The {self.year} {self.make} {self.model} powers on silently.")
def charge(self, hours: float):
"""Charge the battery (unique to electric cars)."""
charge_added = min(hours * 10, 100 - self.battery_level)
self.battery_level += charge_added
print(f"Charged for {hours} hours. Battery: {self.battery_level:.1f}%")
# Inheritance in action
tesla = ElectricCar("Tesla", "Model S", 2023, 100.0)
tesla.start_engine() # Uses overridden method
tesla.drive(50) # Uses inherited method
tesla.charge(2) # Uses unique methodYour turn: vehicle inheritance
Create a new vehicle type: 1. Create a Motorcycle class that inherits from Vehicle 2. Override the start_engine() method with a motorcycle-specific message 3. Add a wheelie() method unique to motorcycles 4. Make motorcycles more fuel-efficient in the drive() method
# Your Motorcycle class hereAdvanced OOP concepts preview
- Class variables: Shared data across all instances of a class
- Properties: Computed attributes using
@propertydecorator - Class methods: Methods that operate on the class itself, not instances
- Static methods: Utility functions that belong logically to the class
- Multiple inheritance: Inheriting from multiple parent classes (advanced topic)
class Product:
"""Represents a product in an inventory system."""
# Class variable - shared across all instances
total_products_created = 0
def __init__(self, name: str, price: float):
self.name = name
self._price = price # Private attribute (convention)
Product.total_products_created += 1
@property
def price(self) -> float:
"""Get the product price."""
return self._price
@price.setter
def price(self, value: float):
"""Set the product price with validation."""
if value < 0:
raise ValueError("Price cannot be negative")
self._price = value
@classmethod
def get_total_products(cls) -> int:
"""Return total number of products created."""
return cls.total_products_created
@staticmethod
def calculate_tax(price: float, tax_rate: float = 0.08) -> float:
"""Calculate tax amount for a given price."""
return price * tax_rate
# Advanced features in action
laptop = Product("Gaming Laptop", 1299.99)
mouse = Product("Wireless Mouse", 79.99)
print(f"Total products: {Product.get_total_products()}") # Class method
print(f"Tax on laptop: ${Product.calculate_tax(laptop.price):.2f}") # Static method
# Property with validation
# laptop.price = -100 # Would raise ValueError
laptop.price = 1199.99 # Valid price changeYour turn: advanced features
Enhance the Product class: 1. Add a stock_quantity attribute and property with validation 2. Create a @classmethod called create_electronics(name, price) that sets category automatically 3. Add a @staticmethod for calculating bulk discount rates
# Your enhanced Product class hereWhere OOP can help
- Web development: Django models, FastAPI dependencies
- Game development: Characters, items, game states, collision systems
- Financial systems: Accounts, transactions, portfolios, risk calculations
- Data science: Custom data structures, machine learning pipelines
- Desktop applications: UI components, event handling, application state
Computer science pioneers: OOP
- Alan Kay (1940-): Coined “object-oriented programming,” created Smalltalk
- Kristen Nygaard (1926-2002): Co-invented Simula, the first OOP language
- Ole-Johan Dahl (1931-2002): Co-creator of Simula, Turing Award winner
- Legacy: Their work influenced later object-oriented languages and interfaces
- Design idea: Objects combine state with behavior exposed through methods
Introducing Lab 06: part 2
- Build on Part 1: Add
get_age(), which returns the years since publication - Current year: Use
date.today().year, not a hardcoded year; the tests derive the expected age the same way - Inheritance practice: Create an
EBookclass that inherits fromBook - Method override: Customize
__str__in the child class; it must include the file size andMB - Required skills: Reuse the parent initializer and inherit
get_agewithout redefining it - AI collaboration: Use AI assistants to explore different implementation approaches
Your turn: complete Lab 06
Create your own implementation of the Book and EBook classes following the exact lab requirements: 1. Implement the Book class with the specified attributes and methods 2. Implement the EBook class that inherits from Book 3. Test both classes thoroughly 4. Try adding additional features like a AudioBook class
# Your complete Lab 06 implementation here
# This is your chance to practice everything you've learned!
if __name__ == '__main__':
# Test your classes here
passLooking ahead: OOP in practice
- Next week: Working with data - APIs, JSON, and object serialization
- Project applications: Your final projects will benefit from OOP design
- Further study: Explore composition, protocols, and selected design patterns
- AI enhancement: Use AI tools to refine your OOP designs and implementations
Summary: key takeaways
- OOP motivation: Can give related state and behavior a clear interface
- Core concepts: Classes (blueprints), objects (instances), attributes (data), methods (behavior)
- Common methods:
__init__initializes instances;__str__supplies a human-readable representation when useful - Inheritance: Enables code reuse and hierarchical relationships
- Design judgment: Use a class when it gives related state and behavior a clearer interface
Time for Lab 06
Deliverable: week06/lab06.py containing Book and EBook.
Bookstorestitle,author, andyear, and its__str__includes all threeget_age()returns the years since publication, usingdate.today().yearEBookextendsBookwithfile_size, reuses the parent initializer viasuper(), and inheritsget_age- Do not copy or modify the supplied tests
uv run --directory week06 python -m pytest tests/ -v
git add week06/lab06.py
git commit -m "Complete Lab 06"
git push origin mainA green Week 06 badge earns the complete 10 points.
Instructions: week06/lab06.md