4

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.

Marla
  • 340
  • 3
  • 16
  • 2
    Your second version doesn't work because you use the first value in your array as your initial minimum. Use `float('inf')` and see what happens. When you set your minimum to 2, 53 is _not_ less than 2. You never find a value you can reset to. – g.d.d.c Apr 24 '18 at 20:12

2 Answers2

8

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:

  • Rewrite minvalue around reduce.
  • Rewrite minvalue to use a "smaller than anything" value.
    • float('inf') will work if all of your values are ints or floats.
    • If the values can be anything at all, you can define a BiggestThing class with a custom __lt__ method, and use BiggestThing().
  • Figure out all the different options for what to do with an empty input—or an input that isn't empty but doesn't have any values that pass the filter—and how to implement them.
  • Try using a sorted data structure, like the one in 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.
abarnert
  • 354,177
  • 51
  • 601
  • 671
4

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
pault
  • 41,343
  • 15
  • 107
  • 149
  • Thanks! If the initial value is <53, why doesn't the second condition, x >= mypass, get evaluated? – Marla Apr 25 '18 at 06:55
  • Python boolean operators [support short circuiting](https://stackoverflow.com/a/14892812/5858851)- so in an `and` operation, `False` will be returned once the first `False`-y value is encountered. But in this case, the non-evaluation of `x>=mypass` does not make a difference because both conditions have to be `True`. So even if you reversed the order of the checks, you'd have the same result. Does this make sense? – pault Apr 25 '18 at 13:27