This thread 𧡠will review some of your `argmax` implementations.
I'll also tell you what's good β and bad β about them.
I'll also tell you which one I think is the best, most Pythonic β¨ one.
Let's go π
The `argmax` function returns the index of the maximum element of the argument.
So, why don't we write this solution?
Compute the maximum, and then use `.index` to get its index! π
But there are two issues here...
Can you see them? π
π first issue is that it traverses the argument twice;
(once to get the max, one more time to find its index)
π second issue is that it assumes the argument has a method `.index`, which might not be the case.
However, let's try and fix the first problem first...
If we only want to go over the argument once, then we can use a `for` loop to keep track of the maximum and its index!
Here's how you could write it π
This goes over the argument only once β
But if you've been paying attention to my tweets, you can improve it right away:
Instead of using `range(len(...))` which is almost always an anti-pattern β...
We can use `enumerate` π
This cleans up the code a little bit β
And it has another interesting effect...
By using `enumerate`, we no longer need to index into the argument to fetch the elements β
Why is this good?
Because it means we can use MORE types of iterables as input.
But for this to be true, we need to get rid of the initial indexing in the first line π
By using `float("-inf")`, we initialise the `mx` variable to something that will always be smaller than whatever is in the argument.
This means that the first iteration of the loop will *always* update the `mx` and `idx` variables.
Now, there's another fundamental difference between this solution and the previous ones.
The previous ones threw an error when the argument was empty.
This one returns -1 π
Empty sequences do NOT have an argmax, so we want our function to raise an error when that happens.
How can we do this..?
Remember when we initialised `mx` with `lst[0]`?
We can do a βsimilarβ thing, but that works on all types of iterables!
We just need to use `next` π
By using `next` on the iterator over the argument, we get a `StopIteration` error if the argument is empty.
Now, callers of the `argmax` function can know if it failed or not π
We are at a pretty good point, by now.
We have a function that works well, for a variety of input types.
Can we make it better..?
I say we can!
Let's talk about dictionary ordering π
Not `dict`.
Dictionary, as in, the book with the words!
Tuples are ordered like dictionaries; did you know that?
If you have a pair `(fst, snd)`, then the tuple is ordered by the value in `fst`.
If it matches the first element of the other pair, then the second element is used as a tie-breaker π
Now, this is a technique I have seen often:
Let's put both things together:
π we take the values that define the ordering (the values of the argument);
π we take the values that we want to return (the indices);
and we zip them together.
Then, we call `max` π π
Again, the problem here is that we are using `range(len(...))`, which is not ideal β
Why?
Because not all iterables have a length.
Sadly, `enumerate` builds the tuples in the wrong order (first indices, then elements)...
But we could flip them! π
But now...
We have the *initial problem* again β
We are going over the argument twice!
Once to build the enumeration with the correct ordering.
And another time to find the maximum!
Thankfully, we can fix this π
How?
Easy!
The `max` function accepts an optional argument called `key`.
We can use the `key` argument to change what/how the elements are compared within max!
For example, if `key` is the `neg` operator (the operator that negates numbers) we can use `max` to compute the `min` π
What we have to do is the following:
Use the regular enumeration we get.
But use `key` to tell `max` to look at the values (the second element of the tuple) instead of the indices!
π
We can do this with a little `lambda` π
Beautiful π₯
But...
Let's make it just a little bit more pretty!
The `operator` module has an operator that renders our lambda useless!
Also, if we are not using `mx`, no need to assign to it, we can just use a sink variable.
In the end, this is what we get π
What a beautiful solution:
β errors on empty sequences
β traverses argument only once
β makes good use of built-ins
β leverages the `key` argument from `max`
β accepts a wide range of argument types
Do you like this solution?
I do π
Thank you to everyone who participated!
For reference, here is the original challenge, where you will find people who proposed really great solutions π
Would you like me to teach you how to write Python π code that is more elegant, more effective, and more expressive than what you have been writing until now... for free?
I am looking to teach Python to people who want to master Python to the best of their abilities.
Do you? If you do, then I know I can help you improve your skills.
How do I know that?
My book has over 10,000 readers, and all of them have one thing in common: they are humans.
Python has dozens of built-ins, and all of them have multiple usage patterns. No human knows all of them by heart, but my research bundled them up nicely in a book for you to reference and learn from.