back

python workout: exercise 10

summing anything

problem

This challenge asks you to redefine the mysum function 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 string abcdef, and the result of mysum([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 integer 6 if we invoke mysum(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 as mysum, except that it takes a first argument that precedes *args. That argument indicates the threshold for including an argument in the sum. Thus, calling mysum_bigger _than(10, 5, 20, 30, 6) would return 50 – because 5 and 6 aren’t greater than 10. 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
    55
    

    And it’s not that straightforward.

    Because the initial value of output might be below the threshold. Begging the question: what should we initialise output to?

    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 filter to our items and just reuse mysum’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))
    
    50
    

    We 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 return 60. Notice that even if the string 30 is 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 try block and catch for the TypeError that would be thrown in casting an object to int that 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}
    
mail@jonahv.com