A little modification: max()
and min()
suffers by a subtle bug: if the first elements are NaNs, NaN is returned.
Why? Because the first element is taken as the default value for max and min. After the functions check if the second element is greater or lesser, and if yes, they substitute the max or min value with the second value, and so on.
The problem is NaN returns always zero for every comparison. So the first element, a NaN, will be never greater or lesser than the other numbers in the sequence, and will be never replaced.
This is the code that fixes this bug:
_sentinel = object()
def minmax(*args, key=None):
args_len = len(args)
if args_len == 0:
fname = minmax.__name__
raise ValueError(f"{fname}() expected 1 argument, got 0")
elif args_len == 1:
seq = args[0]
else:
seq = args
it = iter(seq)
vmax = vmin = next(it, _sentinel)
if vmax is _sentinel:
fname = minmax.__name__
raise ValueError(f"{fname}() arg is an empty sequence")
first_comparable = False
if key is None:
for val in it:
if val > vmax:
vmax = val
first_comparable = True
elif not first_comparable and not val <= vmax:
# not comparable object, like NaN
vmax = vmin = val
continue
elif val < vmin:
vmin = val
first_comparable = True
else:
fmax = fmin = key(vmax)
for val in it:
fval = key(val)
if fval > fmax:
fmax = fval
vmax = val
first_comparable = True
elif not first_comparable and not fval <= fmax:
fmax = fmin = fval
vmax = vmin = val
continue
elif fval < fmin:
fmin = fval
vmin = val
first_comparable = True
return (vmin, vmax)
This slows down the function, but only a little (5.57 ms ± 0.24 ms)
I hope this simple fix will be applied also to min()
and max()
.