Search code examples
pythonif-statementnonetype

Min Max like function, using if best == None with the condition inside the loop in Python


In function where we need to search for the best element according to a condition (like Min Max functions: search the min or the max), I saw some (or many) peoples check if the variable that hold the best answer is None inside the loop side by side of the condition checking.

To illustrate the point, let the following 2 source code:
A) use the test is None inside the loop:

list_vals = [10,9,8,7,6,5,4,3,2,1]

min_val = None

for val in list_vals:
    if min_val is None or val<min_val:
        min_val = val

print("min = ", min_val)

B) The best recive the first element

list_vals = [10,9,8,7,6,5,4,3,2,1]

min_val = list_val[0]

for val in list_vals:
    if val<min_val:
        min_val = val

print("min = ", min_val)

My Questions are:

  • Why some peoples are doing the first form (A) ? is it some good practice ?
  • If we use the first from, that's mean on each iteration we check if the variable is None, I don't know if the compiler optimize this or not.

To see for this example if the execution time is similaire or not, I computed the time for both method A and B with

nb = 100000000 # 100_000_000
list_vals = random.sample(range(nb), nb)
  • time(A) = 24.00 s
  • time(B) = 19.31 s

I repeated the test with other length, and the result is the same, the B method is alyse (relatively) faster than the A method.

Thank you for your help.

Edit one: let assume that list_vals is not empty. Thank you @0x5453


Solution

  • In general code, you may want to work with arbitrary iterables, not just list, in which case only the first form works at all (you can't index non-sequences). You may also need to handle potentially empty inputs (A handles them by producing None, while B raises an IndexError). It does add non-zero cost (CPython has only the simplest, most local of optimizers; it can't make broad deductions like "min_val is only None on the first loop"). If performance is critical, you can get the best of both worlds (though with slightly uglier code) this way:

    vals = ... could be any iterable ...
    
    iter_vals = iter(vals)  # Explicitly convert to iterator (if already an iterator, just returns vals at trivial cost)
    
    min_val = next(iter_vals, None)  # Pulls first value, or None if vals was empty
    
    for val in iter_vals:  # Iterates rest of values looking for minimum
        if val < min_val:
            min_val = val
    
    print("min = ", min_val)
    

    In this scenario, you don't make assumptions about the input being a sequence, you make no unnecessary copies of data (which slicing list_vals[1:] would do), you don't have to invent a sentinel value for the initial case, because you can safely acquire the first element once, and you don't test the initial value against itself (because stateful iterators only produce the initial value once).