Midterm Review: Python Problem Sets 4
Problem 1: Sort Tuples by Second Element
Section titled “Problem 1: Sort Tuples by Second Element”Write a function that sorts a list of tuples by their second element in ascending order.
def sort_by_second(data: list[tuple]) -> list[tuple]: # Your code hereExample usage:
data = [("a", 3), ("b", 1), ("c", 2)]print(sort_by_second(data)) # Output: [("b", 1), ("c", 2), ("a", 3)]Solution
def sort_by_second(data: list[tuple]) -> list[tuple]: return sorted(data, key=lambda x: x[1])Alternatively, using list.sort():
def sort_by_second(data: list[tuple]) -> list[tuple]: data.sort(key=lambda x: x[1]) return dataExplanation: Both the sorted() function and the .sort() method sort the tuples using a lambda function as the key. The key lambda x: x[1] extracts the second element of each tuple for comparison. sorted() returns a new sorted list, while .sort() sorts the list in place.
Problem 2: List Comprehension for Divisible Numbers
Section titled “Problem 2: List Comprehension for Divisible Numbers”Create a list comprehension to generate all numbers divisible by 3 in a given range, eg. 1 to 100 inclusive.
def generate_divisible_by_3(lowerbound: int, upperbound: int) -> list[int]: # Your return statement hereExample usage:
print(generate_divisible_by_3(1, 12)) # Output: [3, 6, 9, 12]Solution
def generate_divisible_by_3(lowerbound: int, upperbound: int) -> list[int]: return [x for x in range(lowerbound, upperbound + 1) if x % 3 == 0]Explanation: The list comprehension iterates through the range from lowerbound to upperbound (inclusive) and includes only numbers where x % 3 == 0 (divisible by 3).
Problem 3: Reverse List Slice
Section titled “Problem 3: Reverse List Slice”Write a function that extracts and reverses a slice of a list (start to end indices inclusive). It should handle out-of-bound cases gracefully. You are not allowed to use the built-in slice operations.
def reverse_slice(lst: list[int], start: int, end: int) -> list[int]: # Your code hereExample usage:
lst = [10, 20, 30, 40, 50]print(reverse_slice(lst, 1, 3)) # Output: [40, 30, 20]print(reverse_slice(lst, 2, 8)) # Output: [50, 40, 30]Solution
def reverse_slice(lst: list[int], start: int, end: int) -> list[int]: # Clamp indices to valid range start = max(0, min(start, len(lst) - 1)) end = max(0, min(end, len(lst) - 1))
# Extract and reverse result = [] for i in range(end, start - 1, -1): result.append(lst[i]) return resultExplanation: The function first clamps the start and end indices to valid ranges (0 to len(lst)-1). Then it iterates from the end index down to the start index and appends each element to the result list, effectively reversing the slice.
Problem 4: Apply Transformation Callback
Section titled “Problem 4: Apply Transformation Callback”Write a Python function apply_transformation that accepts a list of integers and a callback function. apply_transformation should return a new list where each element is transformed according to the callback function. The callback function itself should be one that accepts an integer value and returns the transformed value.
Your function should have the following signature:
def apply_transformation(numbers, transform_function): # Your code hereExample usage:
my_list = [1, 2, 3]number_doubler_lambda = ... # create your lambda herenumber_squarer_lambda = ... # create your lambda here
doubled_nums = apply_transformation(my_list, number_doubler_lambda)squared_nums = apply_transformation(my_list, number_squarer_lambda)
print(doubled_nums) # outputs: [2, 4, 6]print(squared_nums) # outputs: [1, 4, 9]Solution
Solution:
def apply_transformation(numbers, transform_function): return [transform_function(x) for x in numbers]Example usage:
my_list = [1, 2, 3]number_doubler_lambda = lambda x: x * 2number_squarer_lambda = lambda x: x ** 2
doubled_nums = apply_transformation(my_list, number_doubler_lambda)squared_nums = apply_transformation(my_list, number_squarer_lambda)
print(doubled_nums) # [2, 4, 6]print(squared_nums) # [1, 4, 9]Explanation: The function uses a list comprehension to apply the callback function to each element in the numbers list.
The following would be equivalent to the above:
def number_doubler(x): return x * 2
def number_squarer(x): return x ** 2
my_list = [1, 2, 3]
doubled_nums = apply_transformation(my_list, number_doubler_lambda)squared_nums = apply_transformation(my_list, number_squarer_lambda)
print(doubled_nums) # [2, 4, 6]print(squared_nums) # [1, 4, 9]Bonus: Would these function definitions meet the same purpose?
def number_doubler(x): return lambda x: x * 2
def number_squarer(x): return lambda x: x ** 2Answer to bonus: No, this would not be the same. The functions number_doubler and number_squarer as defined here return lambda functions instead of performing the transformation directly. Basically, they return a reference to a function that performs the operation. To use them, you would need to call the returned function reference separately, which is not the intended behavior for this problem.
Problem 5: Sort by Rightmost Digit
Section titled “Problem 5: Sort by Rightmost Digit”Write an expression that sorts a list of integers based on their right-most digit.
Example:
mylist = [5, 4, 21, 20, 39]# Your expression hereprint(mylist) # output: [20, 21, 4, 5, 39]Solution
mylist = [5, 4, 21, 20, 39]mylist.sort(key=lambda x: x % 10)print(mylist) # [20, 21, 4, 5, 39]Alternatively, using sorted():
mylist = sorted([5, 4, 21, 20, 39], key=lambda x: x % 10)print(mylist) # [20, 21, 4, 5, 39]Explanation: The key lambda x: x % 10 extracts the rightmost digit of each number. The numbers are sorted by their rightmost digit in ascending order: 20 and 21 both end in 0 and 1 (20 < 21), then 4 and 5 (both end in 4 and 5), then 39 (ends in 9).
Problem 6: Mutable Default Argument Behavior
Section titled “Problem 6: Mutable Default Argument Behavior”What is the output of this code, and why?
def func(x=[1,2,3]): x.append(4) return x
print(func())print(func(x=[5,6,7]))print(func())Solution
Output:
[1, 2, 3, 4][5, 6, 7, 4][1, 2, 3, 4, 4]Explanation: This demonstrates the mutable default argument pitfall.
- First call:
func()uses the default list[1, 2, 3], appends 4, returns[1, 2, 3, 4]. This default list is stored in memory. - Second call:
func(x=[5, 6, 7])creates a new list, appends 4, returns[5, 6, 7, 4]. This doesn’t affect the default. - Third call:
func()reuses the same default list from the first call, which is now[1, 2, 3, 4](modified), appends 4, returns[1, 2, 3, 4, 4].
The default list is created once at function definition time and reused across all calls.
Problem 7: Extended Slice Assignment
Section titled “Problem 7: Extended Slice Assignment”What happens when you run this code?
lst = [1, 2, 3, 4, 5]lst[1:4:2] = [10, 20]print(lst)Solution
Output:
[1, 10, 3, 20, 5]Explanation: The slice lst[1:4:2] selects elements at indices 1 and 3 (step of 2 from index 1 to 4, exclusive). This selects the elements at positions 1 and 3, which are 2 and 4. The assignment [10, 20] replaces these two elements with 10 and 20 respectively, resulting in [1, 10, 3, 20, 5].
Problem 8: Unpacking and Sorting
Section titled “Problem 8: Unpacking and Sorting”What is the output of this code?
d = [4, 1, 6]lst = [*d, *sorted(d)]print(lst)Solution
Output:
[4, 1, 6, 1, 4, 6]Explanation: The list d is [4, 1, 6]. The expression [*d, *sorted(d)] unpacks d (giving 4, 1, 6) and then unpacks sorted(d) (which is [1, 4, 6], giving 1, 4, 6). These are combined into a single list: [4, 1, 6, 1, 4, 6].
Problem 9: Multiple Slice Reversal
Section titled “Problem 9: Multiple Slice Reversal”What is printed and why?
lst = [1, 2, 3, 4, 5]new_lst = lst[::-1][:3][::-1]print(new_lst)Solution
Output:
[3, 4, 5]Explanation:
lst[::-1]reverses the list:[5, 4, 3, 2, 1][:3]takes the first 3 elements:[5, 4, 3][::-1]reverses again:[3, 4, 5]
The operations are applied left to right (chained slicing).
Problem 10: Closure with Default Parameter
Section titled “Problem 10: Closure with Default Parameter”What is the output of this code?
def create_multipliers(): multipliers = [] for i in range(3): def multiplier(x, i=i): return x * i multipliers.append(multiplier) return multipliers
funcs = create_multipliers()print([f(2) for f in funcs])Solution
Output:
[0, 2, 4]Explanation: The key here is the default parameter i=i in the function definition. This captures the current value of the loop variable i at the time the function is defined.
- When
i=0: the function multiplies by 0, sof(2)returns2 * 0 = 0 - When
i=1: the function multiplies by 1, sof(2)returns2 * 1 = 2 - When
i=2: the function multiplies by 2, sof(2)returns2 * 2 = 4
If the function definition for multiplier did not include the default parameter i=i, then this could produce a situation referred to as a “late binding” closure, where all functions would use the final value of i (which would be 2), resulting in the print statement print([f(2) for f in funcs]) to output [4, 4, 4]
Problem 11: Multiple Sorts
Section titled “Problem 11: Multiple Sorts”What does this code print?
items = [(4, 444), (1, 11), (3, 3333)]sorted_items = sorted(sorted(items, key=lambda x: x[1]), key=lambda x: x[0])print(sorted_items)Solution
Output:
[(1, 11), (3, 3333), (4, 444)]Explanation: The code performs two sorts:
- First
sorted(items, key=lambda x: x[1])sorts by the second element (444, 11, 3333):[(1, 11), (3, 3333), (4, 444)] - Then
sorted(..., key=lambda x: x[0])sorts by the first element (1, 3, 4):[(1, 11), (3, 3333), (4, 444)]
The final sort is the one that matters since it overwrites the previous ordering. The items are sorted by their first element in ascending order.
Problem 12: List Comprehension with Sorted
Section titled “Problem 12: List Comprehension with Sorted”What’s the output of this code?
items = [11, 22, 33, 44, 55]counts = [1, 2, 3, 4, 5]result = [(items[i], counts[i]) for i in range(len(sorted(items, reverse=True)))]print(result)Solution
Output:
[(11, 1), (22, 2), (33, 3), (44, 4), (55, 5)]Explanation: The len(sorted(items, reverse=True)) evaluates to len([55, 44, 33, 22, 11]), which is 5. So the range is range(5) which gives indices 0, 1, 2, 3, 4. The list comprehension creates tuples by pairing items and counts at each index: (items[0], counts[0]), (items[1], counts[1]), etc. The sorted() call is only used to determine the length, not to reorder the result.
Problem 13: List Copy in Function
Section titled “Problem 13: List Copy in Function”What’s printed and why?
def func(items, value): items.append(value) return items
numbers = [1, 2, 3]print(func(numbers[:], 4))print(numbers)Solution
Output:
[1, 2, 3, 4][1, 2, 3]Explanation: The key here is numbers[:], which creates a shallow copy of the list. When func(numbers[:], 4) is called, the function receives a copy of the list, not the original. The function appends 4 to the copy and returns it. The original numbers list remains unchanged because the function modified only the copy, not the original.
Problem 14: In-place List Sort in Slice
Section titled “Problem 14: In-place List Sort in Slice”What’s printed?
mylist = [1, 2, 3, 4, 5]mylist[1:4] = sorted(mylist[1:4], reverse=True)print(mylist)Solution
Output:
[1, 4, 3, 2, 5]Explanation:
mylist[1:4]is the slice[2, 3, 4]sorted([2, 3, 4], reverse=True)returns[4, 3, 2]- The assignment
mylist[1:4] = [4, 3, 2]replaces the slice with the sorted (reversed) elements - The result is
[1, 4, 3, 2, 5]
Problem 15: Sorted and Reversed
Section titled “Problem 15: Sorted and Reversed”What’s the output of this code?
nums1 = list(range(5))nums2 = sorted(nums1, reverse=True)[::-1]print(nums1)print(nums2)Solution
Output:
[0, 1, 2, 3, 4][0, 1, 2, 3, 4]Explanation:
list(range(5))creates[0, 1, 2, 3, 4]and assigns it tonums1sorted(nums1, reverse=True)sorts it in descending order:[4, 3, 2, 1, 0][::-1]reverses this sorted list:[0, 1, 2, 3, 4]- So
nums2is[0, 1, 2, 3, 4] - Both
nums1andnums2are the same
Note that sorted() creates a new list, so nums1 is not modified. Both variables end up with the same list contents.