Week 7 Lecture: Revision Week
These notes cover the core concepts reviewed during the Week 7 lecture session. The goal is to solidify your understanding of the foundational topics from Weeks 1-6 in preparation for the mid-term exam.
1. Control Flow (Loops & Conditionals)
Theory & Explanation
- Definition: Control Flow is the order in which program statements are executed. By default, Python runs code sequentially (top to bottom). Control flow structures like loops and conditionals allow us to alter this default order.
- Importance:
- Conditionals (
if/elif/else) enable programs to make decisions and run different code blocks based on whether a condition isTrueorFalse. - Repetition Structures (
for/while) automate repetitive tasks, which is a fundamental strength of programming.
- Conditionals (
- Relation to Previous Concepts: We use variables, relational operators (
>,==, etc.), and logical operators (and,or) to create the conditions that guide our control flow structures.
Syntax Overview
Conditional Statements
if condition1:
# This block runs if condition1 is True.
elif condition2:
# This block runs if condition1 is False and condition2 is True.
else:
# This block runs if all previous conditions were False.
Note: Indentation is crucial. It defines which lines of code belong to which block.
for Loop (with range)
for variable in range(start, stop, step):
# This block executes for each number in the sequence.
Note: The stop value is exclusive (the loop runs up to, but not including, stop).
while Loop
while condition:
# This block executes as long as the condition remains True.
- Common Mistake: Forgetting to update the variable that controls the
whileloop’s condition, which results in an infinite loop.
Example Problem: Analyzing a List of Numbers
- Problem Statement: Write a function
analyze_numbers(numbers)that takes a list of integers. The function should iterate through the list and count numbers in three categories: (1) positive and even, (2) positive and odd, and (3) negative. The number zero should be ignored. The function must return a tuple containing the three counts in that order:(positive_even_count, positive_odd_count, negative_count). -
Example:
analyze_numbers([1, 2, 3, 4, 5, 6, -1, -2, 0])should return(3, 3, 2). - Solution Code:
def analyze_numbers(numbers): """ Analyzes a list of numbers and categorizes them. """ positive_even_count = 0 positive_odd_count = 0 negative_count = 0 for num in numbers: if num > 0: # A nested conditional to check for even or odd if num % 2 == 0: positive_even_count += 1 else: # num must be odd positive_odd_count += 1 elif num < 0: negative_count += 1 # Note: The case where num == 0 is implicitly ignored. return (positive_even_count, positive_odd_count, negative_count) # --- Testing the function --- test_data = [1, 2, 3, 4, 5, 6, -1, -2, 0, 10, -5] result = analyze_numbers(test_data) print(result) # Expected output: (4, 3, 3) - Code Breakdown:
- Initialization: The three counter variables are initialized to
0before the loop begins. This is essential for any counting or summation logic. - List Traversal: A
forloop is used to process each item in the input listnumbersone by one. - Conditional Logic: The main
if/elifstructure categorizes each number as positive or negative. - Nested Logic: Inside the
if num > 0:block, a second, nestedif/elsestatement is used to further differentiate between even and odd numbers. - Ignoring a Case: Because there is no
elseblock to catchnum == 0, those values are skipped, satisfying the problem requirement. - Return Value: The function concludes by returning a tuple containing the final counts, which neatly packages the multiple results.
- Initialization: The three counter variables are initialized to
2. Functions
Theory & Explanation
- Definition: A function is a named, reusable block of code that performs a specific, well-defined task.
- Importance: Functions are the foundation of organized, readable, and maintainable code.
- Abstraction: Hides complex logic behind a simple, descriptive name.
- Reusability: Allows you to write code once and call it multiple times from different places (the DRY principle: Don’t Repeat Yourself).
Syntax Overview
def function_name(parameter1, parameter2):
"""A docstring explaining what the function does."""
# Body of the function
# ...
return some_value # Optional: sends a value back
- Parameters vs. Arguments: In the definition,
parameter1is a parameter (a placeholder). When you call the function, likefunction_name(10, "hello"), the values10and"hello"are arguments. returnvs.print: This is a critical distinction.print()only displays a value to the console.returnsends a value back to the calling code, allowing it to be stored in a variable or used in other calculations. A function without an explicitreturnstatement implicitly returnsNone.- Variable Scope: Variables created inside a function are local and cannot be accessed from outside.
Example Problem: Calculating an Adjusted Grade
- Problem Statement: Write a function
calculate_grade(scores)that takes a list of numerical scores. It should handle three cases:- If the list is empty, return the string
"No scores". - If the list has only one score, drop it. Since the list is now empty, return
0.0. - Otherwise, drop the single lowest score and return the average of the remaining scores.
- If the list is empty, return the string
-
Example:
calculate_grade([80, 90, 100, 70])should drop70and return90.0. - Solution Code:
def calculate_grade(scores): """ Calculates the average of a list of scores after dropping the lowest score. """ # Edge Case 1: Handle an initially empty list. if not scores: # An empty list evaluates to False return "No scores" # The .remove() method modifies the list in-place. # To avoid this, work on a copy: scores_copy = scores.copy() lowest_score = min(scores) scores.remove(lowest_score) # Edge Case 2: Handle a list that becomes empty after removing an item. if not scores: return 0.0 average = sum(scores) / len(scores) return average # --- Testing the function --- scores1 = [80, 90, 100, 70] print(calculate_grade(scores1)) # Expected output: 90.0 - Code Breakdown:
- Defensive Programming: The function first checks for an empty list (
if not scores:). This is a “guard clause” that handles invalid input immediately and makes the rest of the function cleaner. - Using Built-in Functions: Python’s powerful built-in functions like
min(),sum(), andlen()simplify the code significantly. - List Mutability: The
.remove()method modifies the list it’s called on directly. This is called a “side effect.” If the original list needed to be preserved, we would have to operate on a copy (scores.copy()). - Handling Edge Cases: The code correctly handles two different edge cases: an initially empty list and a list that becomes empty after the lowest score is removed. Robust functions anticipate such cases.
- Defensive Programming: The function first checks for an empty list (
3. List Manipulation
Theory & Explanation
- Definition: A list is a mutable (changeable), ordered sequence of elements. Lists are one of Python’s most common and versatile data structures for storing collections of data.
- Importance: Most programs work with collections of data, not just single values. Lists allow us to store, access, and manipulate these collections efficiently.
Syntax Overview
- Creation:
my_list = [1, "a", True] - Indexing:
my_list[0](first element),my_list[-1](last element) - Slicing:
my_list[1:3](creates a new list with elements from index 1 up to, but not including, index 3) - Common Methods:
.append(item): Adds an item to the end..insert(index, item): Inserts an item at a specific position..pop(): Removes and returns the last item..remove(value): Removes the first occurrence ofvalue.
Example Problem: Filtering and Transforming Data
-
Problem Statement: Write a function
filter_and_double(data)that takes a list of integers. It should create and return a new list containing only the odd numbers from the original list, with each of those numbers doubled. The original list must not be changed. -
Example:
filter_and_double([1, 2, 3, 4, 5])should return[2, 6, 10]. - Solution Code:
def filter_and_double(data): """ Creates a new list containing doubled odd numbers from the input list. """ new_list = [] # Initialize an empty list to accumulate results for number in data: # Step 1: Filter (select only odd numbers) if number % 2 != 0: # Step 2: Transform (double the number) and append doubled_value = number * 2 new_list.append(doubled_value) return new_list # --- Testing the function --- original_data = [1, 2, 3, 4, 5, 6, 7] processed_data = filter_and_double(original_data) print(f"Original: {original_data}") # Expected: [1, 2, 3, 4, 5, 6, 7] print(f"Processed: {processed_data}") # Expected: [2, 6, 10, 14] - Code Breakdown:
- The Accumulator Pattern: This is a fundamental programming pattern. We start with an empty collection (
new_list = []), loop through our data, and conditionally add processed items to the collection. The final collection is then returned. - Non-Destructive Operation: The function creates and returns a new list. The original
datalist is read but never modified. This is good practice, as it prevents unexpected side effects in other parts of a program. - Separation of Logic: The logic inside the loop is clear and separated: first, we filter with an
ifstatement, and second, we transform the data (* 2) before appending it.
- The Accumulator Pattern: This is a fundamental programming pattern. We start with an empty collection (
4. Nested Lists & Linear Search
Theory & Explanation
- Nested Lists: A nested list is a list that contains other lists as elements. This is the standard way to represent 2D data like grids, matrices, or tables (data with rows and columns).
- Linear Search: This is our first formal algorithm. It finds a target value in a list by checking each element one by one, from start to finish, until a match is found or the list ends.
Syntax Overview
- Creation:
grid = [[1, 2, 3], [4, 5, 6]] - Accessing: Use two indices:
grid[row][col]. For example,grid[1][0]accesses the element4. - Traversing: Nested loops are used to process every element.
# Using indices for r in range(len(grid)): for c in range(len(grid[r])): print(grid[r][c]) # More Pythonic way (without indices) for row_list in grid: for item in row_list: print(item)
Example Problem: Finding an Item in a Grid
-
Problem Statement: Write a function
find_item_location(warehouse_grid, target_item)that searches a 2D grid for atarget_item. If the item is found, the function should return its(row, column)coordinates as a tuple. If the item is not in the grid, it should returnNone. -
Example: Given
grid = [[101, 102], [201, 202]], callingfind_item_location(grid, 201)should return(1, 0). - Solution Code:
def find_item_location(warehouse_grid, target_item): """ Performs a linear search on a nested list to find an item's location. """ num_rows = len(warehouse_grid) for r in range(num_rows): num_cols = len(warehouse_grid[r]) for c in range(num_cols): # Core logic: check if the current element is the target if warehouse_grid[r][c] == target_item: return (r, c) # Found: exit immediately with the coordinates # This line is only reached if the loops finish without finding the item return None # --- Testing the function --- warehouse = [[11, 23, 76], [45, 98, 50], [88, 62, 37]] print(find_item_location(warehouse, 98)) # Expected output: (1, 1) print(find_item_location(warehouse, 100)) # Expected output: None - Code Breakdown:
- Nested Iteration: The outer loop iterates through the row indices (
r), and the inner loop iterates through the column indices (c). Together, they visit every cell in the grid. - Accessing by Index: The expression
warehouse_grid[r][c]is used to access the value at the current rowrand columnc. - Efficient Exit: As soon as the
target_itemis found,return (r, c)is executed. This immediately stops both loops and exits the entire function, making the search efficient. - Handling the “Not Found” Case: The
return Nonestatement is placed after the loops have completed. It will only ever be reached if theifcondition was never met, meaning the item was not found anywhere in the grid.
- Nested Iteration: The outer loop iterates through the row indices (
Part 2: Consolidation Problems & Solutions
These problems are designed to test your ability to integrate multiple concepts from the first six weeks. Each solution combines loops, conditionals, functions, and data structures (lists, tuples) to solve a more complex task.
Problem 1: Student Score Processor
- Concepts Practiced: Functions, List of Tuples, Tuple Unpacking, Conditionals, Looping, Accumulator Pattern.
Problem Statement
You have student score data stored as a list of tuples, where each tuple is (student_id_string, score_integer). A single student may have multiple scores recorded.
Write a function get_student_summary(all_scores, target_student_id) that does the following:
- Accepts the list of score tuples and a
target_student_idstring. - Finds all scores belonging to the
target_student_id. - Calculates both the average of that student’s scores and the total count of their scores.
- Returns a tuple containing
(score_count, average_score). - If the
target_student_idis not found, the function should return(0, 0.0).
Example:
Given scores_data = [('101', 88), ('102', 95), ('101', 92)]:
get_student_summary(scores_data, '101')should return(2, 90.0).get_student_summary(scores_data, '999')should return(0, 0.0).
Solution & Explanation
def get_student_summary(all_scores, target_student_id):
"""
Finds all scores for a specific student, calculates the count and average.
"""
# 1. Accumulator Pattern: Create an empty list to store matching scores.
student_scores = []
# 2. Traversal & Tuple Unpacking: Loop through the list.
# On each iteration, unpack the tuple into `student_id` and `score`.
for student_id, score in all_scores:
# 3. Filtering: Check if the current student_id matches our target.
if student_id == target_student_id:
student_scores.append(score)
# 4. Process the results:
# This `if` statement elegantly handles the "not found" case. If no
# scores were found, `student_scores` will be empty and evaluate to False.
if not student_scores:
return (0, 0.0)
# 5. Calculation: If scores were found, calculate the count and average.
score_count = len(student_scores)
total_score = sum(student_scores)
average_score = total_score / score_count
return (score_count, average_score)
# --- Testing the solution ---
scores_data = [
('101', 88), ('102', 95), ('101', 92),
('103', 78), ('102', 80), ('101', 75)
]
summary_101 = get_student_summary(scores_data, '101')
summary_999 = get_student_summary(scores_data, '999')
print(f"Summary for student 101: {summary_101}")
print(f"Summary for student 999: {summary_999}")
Expected Output:
Summary for student 101: (3, 85.0)
Summary for student 999: (0, 0.0)
Key Idea: This solution uses a two-stage process. First, it iterates through the raw data to filter and accumulate the relevant information (the scores for one student). Second, it processes that smaller, cleaned-up list to perform the final calculations.
Problem 2: Grid Boundary Checker
- Concepts Practiced: Nested Lists, Functions, Nested Loops, Conditionals, Indexing, Boundary Checking Logic.
Problem Statement
You are given a 2D list of integers (grid) representing a game board, and a coordinate (row, col).
Write a function sum_boundary_values(grid, row, col) that calculates and returns the sum of all values on the “boundary” of that coordinate. The boundary is defined as all cells in the same row AND all cells in the same column as the given (row, col). The value of the cell at (row, col) itself should be excluded from the sum.
Your function must work for any valid coordinate without causing an index error.
Example:
Given game_grid = [[1, 1, 1], [1, 10, 1], [1, 1, 1]]:
sum_boundary_values(game_grid, 1, 1)should sum the elements in row 1(1 + 1)and column 1(1 + 1), excluding the10. The result is4.
Solution & Explanation
def sum_boundary_values(grid, row, col):
"""
Calculates the sum of values in the same row and column as the
given coordinate, excluding the coordinate's value itself.
"""
if not grid:
return 0
total_sum = 0
num_rows = len(grid)
num_cols = len(grid[0]) # Assumes a non-empty, rectangular grid
# 1. Sum the row: Iterate through each column index `c`.
for c in range(num_cols):
# 2. Exclude the center point: Use a conditional to skip the main cell.
if c != col:
total_sum += grid[row][c]
# 3. Sum the column: Iterate through each row index `r`.
for r in range(num_rows):
# 4. Exclude the center point again.
if r != row:
total_sum += grid[r][col]
return total_sum
# --- Testing the solution ---
game_grid = [
[1, 1, 1, 1],
[1, 10, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]
]
result1 = sum_boundary_values(game_grid, 1, 1)
result2 = sum_boundary_values(game_grid, 0, 0)
print(f"Boundary sum at (1, 1): {result1}")
print(f"Boundary sum at (0, 0): {result2}")
Expected Output:
Boundary sum at (1, 1): 6
Boundary sum at (0, 0): 6
Key Idea: The problem can be broken down into two independent parts: summing the relevant row elements and summing the relevant column elements. The crucial detail is the if condition inside each loop, which prevents the value at the target coordinate (row, col) from being added to the sum.
Problem 3: Find Best Value Product
- Concepts Practiced: List of Tuples, Functions, “Find Maximum” Algorithm, Conditionals, Logical Operators.
Problem Statement
Your inventory is a list of tuples, each representing a product: (product_id, category, price, stock_level).
Write a function find_best_value(inventory, target_category, max_price) that finds the “best value” product based on three criteria:
- The product must belong to the
target_category. - The product’s
pricemust be less than or equal tomax_price. - Of all products meeting these criteria, the one with the highest
stock_levelis the “best value”.
The function should return the product_id of the best value product. If no products match the criteria, return None.
Example:
Given inventory_data = [('P101', 'Electronics', 499.99, 15), ('P104', 'Electronics', 399.00, 40)]:
find_best_value(inventory_data, 'Electronics', 500.00)should identify thatP104has a higher stock level (40) thanP101(15) and return'P104'.
Solution & Explanation
def find_best_value(inventory, target_category, max_price):
"""
Finds the product ID of the item with the highest stock that meets
category and price criteria.
"""
# 1. "Find Maximum" Pattern: Initialize variables to track the best-so-far.
# We start with `highest_stock_so_far = -1` because any real stock level
# will be greater than it.
best_product_id = None
highest_stock_so_far = -1
# 2. Iterate and Unpack: Loop through each product tuple.
for product_id, category, price, stock_level in inventory:
# 3. Filter: Check if the current product meets all criteria.
# The `and` operator ensures both conditions must be true.
if category == target_category and price <= max_price:
# 4. Compare: If it's a valid product, check if it's the new "best".
if stock_level > highest_stock_so_far:
# 5. Update: If it is, update both tracking variables.
highest_stock_so_far = stock_level
best_product_id = product_id
# 6. After the loop, `best_product_id` will hold the ID of the best
# product found, or `None` if no products matched the criteria.
return best_product_id
# --- Testing the solution ---
inventory_data = [
('P101', 'Electronics', 499.99, 15), ('P102', 'Books', 24.50, 80),
('P103', 'Electronics', 549.99, 25), ('P104', 'Electronics', 399.00, 40),
('P105', 'Books', 19.99, 120), ('P106', 'Home Goods', 89.99, 50)
]
best_electronic = find_best_value(inventory_data, 'Electronics', 500.00)
best_book = find_best_value(inventory_data, 'Books', 30.00)
best_toy = find_best_value(inventory_data, 'Toys', 100.00)
print(f"Best value electronic under $500: {best_electronic}")
print(f"Best value book under $30: {best_book}")
print(f"Best value toy under $100: {best_toy}")
Expected Output:
Best value electronic under $500: 'P104'
Best value book under $30: 'P105'
Best value toy under $100: None
Key Idea: This solution implements the classic “Find Maximum” algorithm. It maintains “state” using the best_product_id and highest_stock_so_far variables. As it iterates through the list, it compares each valid item to the best one it has seen so far, and updates its state only when it finds a better item. The initial values for the state variables are chosen to ensure the very first valid item will become the “best-so-far”.
This content will be available starting November 11, 2025.