Midterm Review: Python Problem Sets 4
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
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
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
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 here
number_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 * 2
number_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
Write an expression that sorts a list of integers based on their right-most digit.
Example:
mylist = [5, 4, 21, 20, 39]
# Your expression here
print(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
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
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
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
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
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
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
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
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
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
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.