back

python workout: exercise 9

first-last

problem

Write a function, firstlast, that takes a sequence (string, list, or tuple) and returns the first and last elements of that sequence, in a two-element sequence of the same type. So firstlast('abc') will return the string ac, while firstlast([1,2,3,4]) will return the list [1,4].

attempts

We can just use slices to get the first and last element, the real challenge to me seems to be returning a sequence of the same type as the input.

The first way of doing this that comes to mind is to just use a case statement:

from collections.abc import Sequence
def firstlast(seq: Sequence) -> Sequence:
    first, last = seq[0], seq[-1]
    match seq:
        case str():
            return f'{first}{last}'
        case list():
            return [first, last]
        case tuple():
            return first, last
        case _:
            raise TypeError(f"Unkown sequence type: {type(seq)}")

print(firstlast([1,2,3,4]))
print(firstlast('abcd'))
print(firstlast((5,6,7,8)))
[1, 4]
ad
(5, 8)

Got this matching from Python: match/case by type of value. It’s apparently a “class pattern” compound statement Compound statements — Python 3.13.6 documentation.

I’m not entirely sure how this is working under the hood, but it’s in line with the original proposal: PEP 636 – Structural Pattern Matching.

Regardless, I came up with an alternative solution that shouldn’t require any explicit checks, relying entirely on the slicing and addition operator.

The idea is that slicing a sequence returns a subsequence of the same type. And the addition operator (i.e. +) usually concatenates sequences. So, instead of taking the first and last element of a sequence, we take the subsequence of the first element and the subsequence containing the last element, the + operator will do the job of joining them into a single sequence of the same type:

from collections.abc import Sequence
def firstlast(seq: Sequence) -> Sequence:
    first, last = seq[:1], seq[-1:]
    return first + last

print(firstlast([1,2,3,4]))
print(firstlast('abcd'))
print(firstlast((5,6,7,8)))
[1, 4]
ad
(5, 8)

Now that’s a lot more elegant!

solution

Turns out the second solution was the book’s:

def firstlast(sequence):
    return sequence[:1] + sequence[-1:]

print(firstlast('abcd'))
ad

beyond the exercise

even odd sums

  • problem

    Write a function that takes a list or tuple of numbers. Return a two-element list, containing (respectively) the sum of the even-indexed numbers and the sum of the odd-indexed numbers. So calling the function as even_odd_sums([10, 20, 30, 40, 50, 60]), you’ll get back [90, 120].

  • attempts

    Should be a straightforward implementation of looping over the input:

    def even_odd_sums(numbers: list | tuple) -> list:
        even_sum, odd_sum = 0, 0
        for i, n in enumerate(numbers):
            if i % 2 == 0:
                even_sum += n
            else:
                odd_sum += n
        return [even_sum, odd_sum]
    
    print(even_odd_sums([10,20,30,40,50,60]))
    
    [90, 120]
    

    We could also do away with enumerate and just maitain a boolean to represent which sum to add to. But I don’t think I prefer that.

    Or…

    In the spirit of slices, we could just do the following 1 liner:

    def even_odd_sums(numbers: list | tuple) -> list:
        return [sum(numbers[::2]), sum(numbers[1::2])]
    
    print(even_odd_sums([10,20,30,40,50,60]))
    
    [90, 120]
    

    It’s definitely cleaner. But it comes at the cost of iterating through the collection of numbers twice, I would think.

alternate adding and subtracting

  • problem

    Write a function that takes a list or tuple of numbers. Return the result of alter- nately adding and subtracting numbers from each other. So calling the func- tion as plus_minus([10, 20, 30, 40, 50, 60]), you’ll get back the result of 10+20-30+40-50+60, or 50.

  • attempts

    The funny thing is, it shouldn’t be any different from our previous solution. The commutatitivity of addition and subtraction means that we can group all our additions together and our subtractions together. And since we’re alternating them, our final total will simply be the sum of all the numbers in even places, including the first number), minus the sum of all the numbers in the odd places:

    from numbers import Number
    def plus_minus(numbers: list | tuple) -> Number:
        return numbers[0]+sum(numbers[1::2])-sum(numbers[2::2])
    
    print(plus_minus([10,20,30,40,50,60]))
    
    50
    

pseudo-zip

  • problem

    Write a function that partly emulates the built-in zip function (http://mng.bz/ Jyzv), taking any number of iterables and returning a list of tuples. Each tuple will contain one element from each of the iterables passed to the function. Thus, if I call myzip([10, 20,30], 'abc'), the result will be [(10, 'a'), (20, 'b'), (30, 'c')]. You can return a list (not an iterator) and can assume that all of the iterables are of the same length.

  • attempts

    I remember doing this (or some version of it) at some point in time.

    I’m not too sure the best way of doing this…

    The elephant in the room is how to handle the cases when the input iterables are off different lengths. Do we stop at the shortest one, for example?

    That’s what Python’s zip does, I think. So we might as well do the same.

    The question then whether it’s worth doing anything fancy or are we just going to find out which input iterable is shortest and use a for loop, as so:

    from typing import Iterable
    def myzip(*iters: list[Iterable]) -> list[tuple]:
        shortest_length = min(map(len, iters))
        output = []
        for i in range(shortest_length):
            output.append(tuple(iterable[i] for iterable in iters))
        return output
    
    print(myzip([10,20,30], 'abc'))
    
    [(10, 'a'), (20, 'b'), (30, 'c')]
    

    Look. This works.

    It’s just not particularly clean or clever.

    There has to be a nice way of doing this with slices…

    What about the simple case of pairing 2 iterables in this way?

    I don’t know aobut a slicing solution. But there’s a recursive one that’s a bit more elegant:

    from typing import Iterable
    def myzip(*iters: list[Iterable]) -> list[tuple]:
        try:
            return [tuple(i[0] for i in iters)] + myzip(*[i[1:] for i in iters])
        except IndexError:
            return []
    
    print(myzip([10,20,30], 'abc'))
    print(myzip([10,20], 'abc'))
    
    [(10, 'a'), (20, 'b'), (30, 'c')]
    [(10, 'a'), (20, 'b')]
    

    The only trouble is that Pythons not really built for recursion.

    But I can’t think of another solution for the time being :(

mail@jonahv.com