I'm trying to find the minimum in a list, above a certain threshold without using the min() function (I'm doing things the hard way for practice).
I've managed by first creating a list of values above the threshold and then looping through the list, saving a value to a variable if it is smaller than the previously seen values:
def minpass(mymarks, mypass):
passed= [x for x in mymarks if x >= mypass]
min_value = passed[0]
for x in passed:
if x < min_value:
min_value = x
return min_value
x = [2, 53, 90]
y = 50
minpass(x, y)
This correctly returns 53.
Is it possible to do it without creating a second list (passed)? Why doesn't it work to add a second condition? e.g.
def minpass(mymarks, mypass):
min_value = mymarks[0]
for x in mymarks:
if x < min_value and x >= mypass:
min_value = x
return min_value
x = [2, 53, 90]
y = 50
minpass(x, y)
This incorrectly returns 2 rather than 53.
Since you're doing this as a learning experience:
To avoid creating a second list, the most interesting alternative is to create a lazy iterator instead. Under the covers, this works out the next filtered value on demand, instead of building a list of them up-front. But from much of your code, it can actually look as if you created a list.
There are different ways to create lazy iterators—an explicit iterator class, the filter
builtin, a generator function—but in your case, you can just use a generator expression instead of a list comprehension:
passed = (x for x in mymarks if x >= mypass)
All I had to do was change the square brackets to parentheses, and you've magically got a lazy iterator.
However, an iterator can only be used to go through the values in order, once. You can't do things like indexing (passed[0]
). So you need to rethink your code a bit. But that turns out to be very easy:
def minpass(mymarks, mypass):
passed = (x for x in mymarks if x >= mypass)
min_value = next(passed) # this consumes the first value
for x in passed: # this loops over all the remaining values
if x < min_value:
min_value = x
return min_value
While we're at it, you might want to consider refactoring your code into two functions—write your own minvalue
function that takes any iterable (an iterable is a lazy iterator, or a sequence like a list, or anything else that can go in a for
loop) and returns the minimum value:
def minvalue(it):
it = iter(it) # this makes sure we have a lazy iterator
min_value = next(it) # this consumes the first value
for x in it: # this loops over all the remaining values
if x < min_value:
min_value = x
return min_value
def minpass(mymarks, mypass):
return minvalue(x for x in mymarks if x >= mypass)
Or maybe refactor further:
def passvalues(it, mypass):
return (x for x in it if x >= mypass)
def minpass(mymarks, mypass):
return minvalue(passvalues(mymarks, mypass))
Notice that this approach solves your second problem automatically. Your problem was that mymarks[0]
might not be >= mypass
. To rewrite things to work, you'd have to do something like this:
def minpass(mymarks, mypass):
for x in mymarks:
if x >= mypass:
min_value = x
break
for x in mymarks:
if x < min_value and x >= mypass:
min_value = x
return min_value
But writing things as a chain of iterator transformations forces you to put them in order—do the filtering first, and then the min-finding, which means you're automatically getting the first filtered value rather than the first value—while still interleaving the work (and avoiding the time and space costs of creating a whole unnecessary list) the way you wanted.
If you want a more in-depth introduction to these ideas, David Beazley's Generator Tricks for Systems Programmers is amazing.
One last thing to consider: is there a way to get rid of the special treatment for the first value?
You could start with a value that will be greater than anything, or you could use a flag to specify whether you'd found a minimum value so far:
def minvalue(it):
found_min = False
for x in it:
if not found_min or x < min_value:
min_value = x
found_min = True
return min_value
This has the advantage (or maybe disadvantage, depending on what you wanted…) of still failing when passed an empty list, but it simplifies the looping (no need to pull out the first value, which means no need to call iter
). But the manual flag management might add more noise than it helps you remove. Still, it's worth comparing the two and deciding for yourself.
Other things you might want to consider trying for yourself:
minvalue
around reduce
.minvalue
to use a "smaller than anything" value.
float('inf')
will work if all of your values are ints or floats.BiggestThing
class with a custom __lt__
method, and use BiggestThing()
.heapq
to do minvalue
—then you can expand it to return the two lowest values instead of just the lowest, or take an numlowest
parameter and and return that many.In general, find the min
(or max
) using a loop requires you to initialize the return variable to something huge (or a huge negative value).
In your case you are initializing the minimum to the first element in the list. Then for subsequent elements, the x < min_value
check will evaluate to False
since this value is already the minimum of the whole list.
Here is one way to modify your code:
def minpass(mymarks, mypass):
min_value = 1e10 # something bigger than your maximum value
for x in mymarks:
if x < min_value and x >= mypass:
min_value = x
return min_value
x = [2, 53, 90]
y = 50
minpass(x, y)
#53
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With