10
3825
Fundamentals of Python: First Programs 3rd edition
Effective designers invent useful abstractions to control complexity
What does the following function do?
Things that the calculate_tax(income) function could do:
What does the following function do?
Things that the is_strong_password(password) function could do:
Functions eliminate repetitive code:
10
3825
One function, many uses!
Top-down design breaks a problem into smaller, manageable subproblems
┌─────────────┐
│ main │
└─────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ readFile │ │ analyze │ │ display │
└───────────┘ └───────────┘ └───────────┘
│
┌────┴────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ count │ │ find │
│ words │ │ mode │
└─────────┘ └─────────┘
┌─────────────┐
│ main │
└─────────────┘
│
┌──────┴──────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ sentence │ │ get │
│ generator │ │ input │
└───────────┘ └───────────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ noun │ │ verb │ │ prep │
│ phrase│ │ phrase│ │ phrase│
└───────┘ └───────┘ └───────┘
Problem structure often suggests program structure!
┌─────────────┐
│ main │
└─────────────┘
│
┌──────┴──────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ calculate │ │ get │
│ grades │ │ input │
└───────────┘ └───────────┘
│
┌─────┴─────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ return │ │ return │
│ letter | | file |
│ grade │ | report │
└───────────┘ └───────────┘
Iterative version:
Recursive version:
def summation(lower, upper, margin):
blanks = " " * margin
print(blanks, lower, upper)
if lower > upper:
print(blanks, 0)
return 0
else:
result = lower + summation(lower + 1, upper, margin + 4)
print(blanks, result)
return result
# Output for summation(1, 4, 0):
# 1 4
# 2 4
# 3 4
# 4 4
# 5 4
# 0
# 4
# 7
# 9
# 10Recursive definition: - Fib(1) = 1 (base case) - Fib(2) = 1 (base case) - Fib(n) = Fib(n-1) + Fib(n-2) for n > 2
Recursion flows naturally from problem structure!
Occurs when: - No base case is specified, OR - Problem size doesn’t decrease toward base case
Always ensure your recursion terminates!
Costs: - Each call creates a stack frame (uses memory) - Memory grows with problem size - Slower than iteration for same task
Benefits: - Elegant for naturally recursive problems - Matches problem structure - Easier to understand for some algorithms
| Iteration | Recursion |
|---|---|
| Uses less memory | Uses more memory |
| Faster | Slower |
| Loops | Function calls |
| Good for simple repetition | Good for tree-like problems |
A program’s namespace is the set of its variables and their values
Three levels:
Scope = area where a name refers to a given value
Functions can READ module variables but cannot normally ASSIGN to them
Lifetime = period when variable has memory storage
| Variable Type | Lifetime |
|---|---|
| Module variable | Entire program execution |
| Parameter | During function call |
| Temporary | During function call |
Specify optional arguments with default values:
Print always defaults to \n
For custom functions
def example(required, option1=2, option2=3):
print(required, option1, option2)
example(1) # 1 2 3 (all defaults)
example(1, 10) # 1 10 3 (override first)
example(1, 10, 20) # 1 10 20 (override both)
example(1, option2=20) # 1 2 20 (override by name)
example(1, option2=20, option1=10) # 1 10 20 (any order)1 2 3
1 10 3
1 10 20
1 2 20
1 10 20
Keywords allow skipping defaults!
Functions can be:
4
4.0
Functions are just data!
Mapping applies a function to each value in a sequence and returns results
Filtering applies a predicate (Boolean function) to each value - If predicate returns True, value is kept - If False, value is dropped
Reducing repeatedly applies a function to accumulate a single value
Lambda creates an anonymous function on the fly
Syntax: lambda args: expression
10
24
Lambda must be a single expression (no statements)!
55
Clean, functional style!
Replacing...
Eliminates long if-elif chains!
__str__ methodProcedural (Functions):
Object-Oriented (Classes):
┌─────────────────────┐
│ Student │ ← Class (Blueprint)
├─────────────────────┤
│ - name │
│ - scores │
├─────────────────────┤
│ + getName() │
│ + getAverage() │
│ + setScore() │
└─────────────────────┘
Objects package state and methods in a single entity
# Creating objects from the Student class
student1 = Student("Maria", 5)
student2 = Student("Juan", 5)
# Each object has its own state
student1.setScore(1, 100) # Maria's first score = 100
student2.setScore(1, 85) # Juan's first score = 85
# Same method, different objects, different results
print(student1.getAverage()) # 20.0
print(student2.getAverage()) # 17.0object is the root parent classself as first parameterclass Student(object):
"""Represents a student with test scores"""
def __init__(self, name, number):
"""Creates a student with given name and number of scores"""
self.name = name
self.scores = []
for count in range(number):
self.scores.append(0)
def getName(self):
"""Returns the student's name"""
return self.name
def getScore(self, i):
"""Returns the ith score (1-indexed)"""
return self.scores[i - 1] ┌─────────────┐
│ object │ ← Root of all classes
└─────────────┘
▲
│
┌─────────────┐
│ Student │ ← Our class
└─────────────┘
▲
│
┌─────────────┐
│ GradStudent │ ← Subclass (inherits from Student)
└─────────────┘
Terminology: Subclass, Parent class, Superclass
__init__ Method (Constructor)self refers to the object being createdself.__str__ Methodstr() and print()Accessors: Observe state without changing it
Mutators: Modify object’s state
Tip: Don’t create mutators for attributes that shouldn’t change!
class Student(object):
"""Represents a student with test scores"""
def __init__(self, name, number):
self.name = name
self.scores = []
for count in range(number):
self.scores.append(0)
def __str__(self):
return "Name: " + self.name + "\nScores: " + \
" ".join(map(str, self.scores))
def getName(self):
return self.name
def getScore(self, i):
return self.scores[i - 1]
def setScore(self, i, score):
self.scores[i - 1] = score
def getAverage(self):
return sum(self.scores) / len(self.scores)
def getHighScore(self):
return max(self.scores)__init__ and __str__Represent fractions like 1/2, 2/3, etc.
class Rational(object):
"""Represents a rational number (numerator/denominator)"""
def __init__(self, numerator, denominator):
self.numer = numerator
self.denom = denominator
def __str__(self):
return str(self.numer) + "/" + str(self.denom)
# Usage:
oneHalf = Rational(1, 2)
oneSixth = Rational(1, 6)
print(oneHalf) # 1/2Python allows overloading operators for custom classes
| Operator | Method Name |
|---|---|
+ |
__add__ |
- |
__sub__ |
* |
__mul__ |
/ |
__truediv__ |
== |
__eq__ |
< |
__lt__ |
> |
__gt__ |
x + y becomes x.__add__(y)
def __add__(self, other):
"""Returns sum of two rational numbers"""
newNumer = self.numer * other.denom + other.numer * self.denom
newDenom = self.denom * other.denom
return Rational(newNumer, newDenom)
# Usage:
oneHalf = Rational(1, 2)
oneSixth = Rational(1, 6)
print(oneHalf + oneSixth) # 8/12 (could be reduced)def __eq__(self, other):
"""Tests for equality"""
if self is other: # Same object?
return True
elif type(self) != type(other): # Different types?
return False
else:
# Compare cross-multiplied values
return self.numer * other.denom == other.numer * self.denom
# Usage:
oneHalf = Rational(1, 2)
twoFourths = Rational(2, 4)
print(oneHalf == twoFourths) # True (1/2 == 2/4)def __lt__(self, other):
"""Less than comparison"""
return self.numer * other.denom < other.numer * self.denom
def __le__(self, other):
"""Less than or equal"""
return self < other or self == other
def __gt__(self, other):
"""Greater than"""
return not (self < other or self == other)
# Usage:
print(Rational(1, 2) > Rational(1, 3)) # True
print(Rational(1, 2) < Rational(1, 3)) # Falseclass SavingsAccount(object):
"""Represents a bank account with owner, PIN, and balance"""
RATE = 0.02 # Class variable - same for all accounts!
def __init__(self, name, pin, balance=0.0):
self.name = name
self.pin = pin
self.balance = balance
def __str__(self):
result = "Name: " + self.name + "\n"
result += "PIN: " + self.pin + "\n"
result += "Balance: " + str(self.balance)
return resultClass variable: Shared by ALL instances
Instance variable: Unique to each instance
def deposit(self, amount):
"""Deposits amount and returns None"""
self.balance += amount
return None
def withdraw(self, amount):
"""Withdraws amount, returns None or error message"""
if amount < 0:
return "Amount must be >= 0"
elif self.balance < amount:
return "Insufficient funds"
else:
self.balance -= amount
return None
def computeInterest(self):
"""Computes, deposits, and returns interest"""
interest = self.balance * SavingsAccount.RATE
self.deposit(interest)
return interestclass Bank(object):
"""Represents a collection of savings accounts"""
def __init__(self, fileName=None):
self.accounts = {} # Dictionary: PIN → Account
self.fileName = fileName
def add(self, account):
"""Adds account to bank"""
self.accounts[account.getPin()] = account
def get(self, name, pin):
"""Returns account if name/pin match, else None"""
for account in self.accounts.values():
if account.getName() == name and account.getPin() == pin:
return account
return None>>> from bank import Bank
>>> from savingsaccount import SavingsAccount
>>> bank = Bank()
>>> bank.add(SavingsAccount("Wilma", "1001", 4000.00))
>>> bank.add(SavingsAccount("Fred", "1002", 1000.00))
>>> print(bank)
Name: Fred
PIN: 1002
Balance: 1000.00
Name: Wilma
PIN: 1001
Balance: 4000.00
>>> account = bank.get("Wilma", "1001")
>>> account.deposit(25.00)
>>> print(account)
Name: Wilma
PIN: 1001
Balance: 4025.00Pickling converts objects to storable format
import pickle
def save(self, fileName=None):
"""Saves pickled accounts to a file"""
if fileName != None:
self.fileName = fileName
elif self.fileName == None:
return
fileObj = open(self.fileName, "wb") # 'wb' = write binary
for account in self.accounts.values():
pickle.dump(account, fileObj)
fileObj.close()try-except catches exceptions (like EOFError)!
class Card(object):
"""A playing card with suit and rank"""
RANKS = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
SUITS = ("Spades", "Diamonds", "Hearts", "Clubs")
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __str__(self):
if self.rank == 1:
rank = "Ace"
elif self.rank == 11:
rank = "Jack"
elif self.rank == 12:
rank = "Queen"
elif self.rank == 13:
rank = "King"
else:
rank = self.rank
return str(rank) + " of " + self.suitCard is a container for two data values (rank and suit)
import random
class Deck(object):
"""A deck of 52 playing cards"""
def __init__(self):
self.cards = []
for suit in Card.SUITS:
for rank in Card.RANKS:
self.cards.append(Card(rank, suit))
def shuffle(self):
"""Shuffles the deck"""
random.shuffle(self.cards)
def deal(self):
"""Removes and returns the top card"""
return self.cards.pop()
def __len__(self):
"""Returns number of cards remaining"""
return len(self.cards)class Grid(object):
"""Represents a 2D grid with rows and columns"""
def __init__(self, rows, columns, fillValue=None):
"""Creates a grid with given dimensions"""
self.data = []
for row in range(rows):
self.data.append([fillValue] * columns)
def __str__(self):
"""Returns string representation"""
result = ""
for row in self.data:
result += " ".join(map(str, row)) + "\n"
return result
def __getitem__(self, index):
"""Returns the row at given index"""
return self.data[index]Inheritance allows a class to reuse and extend code from another class
┌─────────────────┐
│ SavingsAccount │ ← Parent class (superclass)
└─────────────────┘
▲
│ inherits from
┌─────────────────┐
│RestrictedAccount│ ← Child class (subclass)
└─────────────────┘
Subclass gets ALL methods and attributes of parent!
class RestrictedSavingsAccount(SavingsAccount):
"""Account with limited monthly withdrawals"""
def __init__(self, name, pin, balance=0.0):
super().__init__(name, pin, balance) # Call parent constructor
self.withdrawals = 0 # Add new attribute
self.MAX_WITHDRAWALS = 3
def withdraw(self, amount):
"""Override withdraw with limit check"""
if self.withdrawals >= self.MAX_WITHDRAWALS:
return "No more withdrawals this month"
else:
self.withdrawals += 1
return super().withdraw(amount) # Call parent method
def resetCounter(self):
"""Resets withdrawal counter"""
self.withdrawals = 0>>> account = RestrictedSavingsAccount("Ken", "1001", 500.00)
>>> print(account) # Inherited __str__
Name: Ken
PIN: 1001
Balance: 500.0
>>> account.getBalance() # Inherited method
500.0
>>> for count in range(3):
... account.withdraw(100) # Works fine (3 times)
>>> account.withdraw(50) # Fourth withdrawal fails
'No more withdrawals this month'
>>> account.resetCounter() # New method
>>> account.withdraw(50) # Works again!super() Functionsuper() gives access to parent class methods
class Child(Parent):
def __init__(self, arg1, arg2, extra_arg):
# Call parent constructor first
super().__init__(arg1, arg2) # Initialize parent part
self.extra = extra_arg # Initialize new part
def overridden_method(self):
# Do something new
result = super().overridden_method() # Call parent version
return result + something_extraAlways call super().__init__() in subclass constructors!
Polymorphism = many forms (same method name, different behaviors)
class Animal:
def speak(self):
return "???"
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class Cow(Animal):
def speak(self):
return "Moo!"
# Polymorphism in action
animals = [Dog(), Cat(), Cow()]
for animal in animals:
print(animal.speak()) # Different output for each!
# Woof!
# Meow!
# Moo!Without polymorphism: (long if-elif chains)
With polymorphism: (clean, extensible)
Add new types without changing existing code!
┌──────────────┐
│ object │
└──────────────┘
▲
│
┌──────────────┐
│ PhysicalObject│
└──────────────┘
▲
│
┌──────────────┐
│ LivingThing │
└──────────────┘
▲
┌────────────┼────────────┐
│ │ │
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Animal │ │ Plant │ │ Fungus │
└──────────┘ └──────────┘ └──────────┘
▲
┌─────┼─────┐
│ │ │
┌─────┐ ┌─────┐ ┌─────┐
│ Dog │ │ Cat │ │Cow │
└─────┘ └─────┘ └─────┘
Each level adds more specific details!
class Blackjack(object):
"""Manages a game of Blackjack"""
def play(self):
"""Runs one game of Blackjack"""
deck = Deck()
deck.shuffle()
player = Player()
dealer = Dealer()
# Deal initial cards
player.addCard(deck.deal())
player.addCard(deck.deal())
dealer.addCard(deck.deal())
# Player's turn
while player.getPoints() < 21:
print("Player:", player)
print("Points:", player.getPoints())
if input("Hit? (y/n): ").lower() != 'y':
break
player.addCard(deck.deal())
# Dealer's turn
while dealer.getPoints() < 17:
dealer.addCard(deck.deal())
# Determine winner
self.determineWinner(player, dealer)| Aspect | Benefit |
|---|---|
| Encapsulation | Data and methods together; hide implementation |
| Inheritance | Reuse code; build hierarchies |
| Polymorphism | Same interface, different behaviors |
| Modularity | Objects are independent units |
| Extensibility | Add new types without breaking existing code |
| Concern | Description |
|---|---|
| Over-engineering | Simple problems don’t need OOP |
| Complexity | More code, more files |
| Performance | Slightly slower than procedural |
| Learning curve | Harder for beginners |
| Overuse | Not everything needs to be a class |
Use OOP when modeling real-world entities with state and behavior!
Use functions when: - Task has no persistent state - Simple transformation of inputs to outputs - Utility operations (math, string manipulation) - Standalone algorithms
Use classes when: - Need to maintain state between operations - Modeling real-world entities - Multiple similar objects with same behavior - Data and operations naturally belong together
| Aspect | Functions | Classes |
|---|---|---|
| State | No persistent state | Maintains state |
| Data | Passed as arguments | Stored in object |
| Organization | By task/operation | By entity/concept |
| Reuse | Call the function | Instantiate objects |
| Complexity | Lower | Higher |
| Best for | Transformations, calculations | Modeling, stateful systems |
Functions approach:
Classes approach:
__init__ is the constructor__str__ provides string representation| Concept | Functions | Classes |
|---|---|---|
| Purpose | Process data | Model entities |
| Reuse | Call function | Instantiate object |
| State | Stateless | Stateful |
| Organization | Task-based | Entity-based |
| Complexity | Simple | Powerful |
Choose the right tool for the job!
Thank you!