python workout: exercise 10
summing anything
problem
This challenge asks you to redefine the
mysumfunction we defined in chapter 1, such that it can take any number of arguments. The arguments must all be of the same type and know how to respond to the+operator. (Thus, the function should work with numbers, strings, lists, and tuples, but not with sets and dicts.)The result should be a new, longer sequence of the type provided by the parameters. Thus, the result of
mysum('abc', 'def')will be the stringabcdef, and the result ofmysum([1,2,3], [4,5,6])will be the six-element list[1,2,3,4,5,6]. Of course, it should also still return the integer6if we invokemysum(1,2,3).
attempts
So it seems we can just loop through all our arguments, maintaining a total sum. That we return once we’re done.
The only question is how we ensure types? How do we ensure the arguments are of type: numbers, strings, lists, or tuples, but not sets or dicts?
My solution?
Just handle the exception!
from numbers import Number
def mysum(*args: Number | str | list | tuple) -> Number | str | list | tuple:
try:
total = args[0]
for a in args[1:]:
total += a
return total
except TypeError as e:
print(f"Couldn't pefore sum operation on inputs: {e}")
print(mysum('abc','def'))
print(mysum([1,2,3], [4,5,6]))
print(mysum(1,2,3))
abcdef
[1, 2, 3, 4, 5, 6]
6
By initialising total to the first argument, we remove the need to intiialise to
the null instance of which type the inputs happen to be. Only drawback is
calling mysum() with no argument will raise an IndexError. But we could
explicitly handle it, if we really wanted:
from numbers import Number
def mysum(*args: Number | str | list | tuple) -> Number | str | list | tuple:
try:
total = args[0]
for a in args[1:]:
total += a
return total
except IndexError as e:
print(f"No arguments provided: {e}")
except TypeError as e:
print(f"Couldn't pefore sum operation on inputs: {e}")
print(mysum())
No arguments provided: tuple index out of range
None
The type hinting could probably use an explicit Union type. I’m happy otherwise.
solution
The book’s implementation:
def mysum(*items):
if not items:
return items
output = items[0]
for item in items[1:]:
output += item
return output
print(mysum())
print(mysum(10, 20, 30, 40))
print(mysum('a', 'b', 'c', 'd'))
print(mysum([10, 20, 30], [40, 50, 60], [70, 80]))
()
100
abcd
[10, 20, 30, 40, 50, 60, 70, 80]
I much prefer the use of returning the empty sequence on empty input. And the
use of if not items to do so, rather than a try block.
I also prefer the naming of output rather than total – it’s more generic.
beyond the exercise
mysum_bigger_than
-
problem
Write a function,
mysum_bigger_than, that works the same asmysum, except that it takes a first argument that precedes*args. That argument indicates the threshold for including an argument in the sum. Thus, callingmysum_bigger _than(10, 5, 20, 30, 6)would return50– because5and6aren’t greater than10. This function should similarly work with any type and assumes that all of the arguments are of the same type.
-
attempts
This should be straightforward:
def mysum_bigger_than(threshold, *items): if not items: return items output = items[0] for item in items[1:]: print(item) if item > threshold: output += item return output print(mysum_bigger_than(10, 5, 20, 30, 6))20 30 6 55And it’s not that straightforward.
Because the initial value of
outputmight be below the threshold. Begging the question: what should we initialiseoutputto?I think we should implement a recursive definition:
def mysum_bigger_than(threshold, *items): if not items: return items if items[0] > threshold: return items[0] + mysum_bigger_than(threshold, items[1:]) return mysum_bigger_than(threshold, items[1:]) print(mysum_bigger_than(10, 5, 20, 30, 6))But that won’t work either. Because of the same reason funnily enough – the bsae case. We need a way of knowing what to return, especially the appropriate type default…
The maybe cheating way would be to apply
filterto ouritemsand just reusemysum’s original logic. The more Pythonic way to do this might be to just use a list comprehension though:def mysum_bigger_than(threshold, *items): items = tuple(item for item in items if item > threshold) if not items: return items output = items[0] for item in items[1:]: output += item return output print(mysum_bigger_than(10, 5, 20, 30, 6))50We end up iterating through our inputs twice. But I like the solution all the same.
In theory, a collection of inputs could all be below the threshold. And we would then be left to realise that there is no valid starting point.
This way, we can screen our inputs before we get to any of the summing logic and handle edge cases before they would throw exceptions in the main body.
sum_numeric
-
problem
Write a function,
sum_numeric, that takes any number of arguments. If the argument is or can be turned into an integer, then it should be added to the total. Arguments that can’t be handled as integers should be ignored. The result is the sum of the numbers. Thus,sum_numeric(10, 20, 'a', '30', 'bcd')would return60. Notice that even if the string30is an element in the list, it’s converted into an integer and added to the total.
-
attempts
The first thing that comes to mind is to just use a
tryblock and catch for theTypeErrorthat would be thrown in casting an object tointthat can’t be turned into an integer.And, as before, we perform this “intability” screen before the main summing logic. To handle edge cases where non of the arguments are numberic:
def sum_numeric(*args): items = [] for arg in args: try: items.append(int(arg)) except TypeError: pass output = items[0] for item in items[1:]: output += item return output print(sum_numeric(10, 20, 'a', '30', 'bcd'))So let’s just catch all exceptions instead:
def sum_numeric(*args): items = [] for arg in args: try: items.append(int(arg)) except Exception: pass output = items[0] for item in items[1:]: output += item return output print(sum_numeric(10, 20, 'a', '30', 'bcd'))60
single dict
-
problem
Write a function that takes a list of dicts and returns a single dict that combines all of the keys and values. If a key appears in more than one argument, the value should be a list containing all of the values from the arguments.
-
attempts
This is about duplication handling.
I think the easiest way to handle this is with
collections.defaultdict:from collections import defaultdict def singledict(dicts: list[dict]) -> dict[list]: combined = defaultdict(list) for d in dicts: for key, value in d.items(): combined[key].append(value) return combined dicts = [{'a': 1, 'b': 2, 'c': 3}, {'z': 26, 'y': 25, 'a': 0}] print(singledict(dicts))defaultdict(<class 'list'>, {'a': [1, 0], 'b': [2], 'c': [3], 'z': [26], 'y': [25]})The only downside of this implementation is that even values that have unique keys across all arguments dictionaries, will have their value stored in a list.
If we don’t want this to be the case, we would probably have to do the following:
from collections import defaultdict def singledict(dicts: list[dict]) -> dict[list]: combined = {} for d in dicts: for key, value in d.items(): if key in combined: if isinstance(combined[key], list): combined[key].append(value) else: combined[key] = [combined[key], value] else: combined[key] = value return combined dicts = [{'a': 1, 'b': 2, 'c': 3}, {'z': 26, 'y': 25, 'a': 0}] print(singledict(dicts)){'a': [1, 0], 'b': 2, 'c': 3, 'z': 26, 'y': 25}