2

I can use functools.reduce or min / max to get min and max of members in a list. But to get both in one pass I need to write a loop:

from functools import reduce

class foo:
    def __init__(self,value): self.value = value
    
x = []
x.append(foo(1))
x.append(foo(2))
x.append(foo(3))

min_value = reduce(lambda a,b: a if a.value < b.value else b,x).value
max_value = reduce(lambda a,b: a if a.value > b.value else b,x).value

print(min_value)
print(max_value)

min_value2 = min(x,key=lambda a: a.value).value
max_value2 = max(x,key=lambda a: a.value).value

print(min_value2)
print(max_value2)

min_value3 = x[0].value
max_value3 = x[0].value
for f in x:
    if f.value < min_value3: min_value3 = f.value
    if f.value > max_value3: max_value3 = f.value
    
print(min_value3)
print(max_value3)

Is it possible to get min and max in one pass without writing a plain loop?

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • I'm not sure it will be very clean when using a lambda, but you could pass a tuple as your aggregator in the `reduce`, where the first value is the min and the second the max. – Nathaniel Ford Apr 14 '22 at 17:15

2 Answers2

2

You could use a tuple as your aggregator. Something like this maybe?

min_value, max_value = reduce(lambda a, b: 
  (a[0] if a[0].value < b.value else b, a[1] if a[1].value > b.value else b), 
  x, 
  (x[0], x[1]))

The output should be a tuple where the first is the minimum and the second the maximum.

Example in the REPL, demonstrating that the objects requested are returned, and that the values are correct:

>>> class Foo:
...     def __init__(self,value): self.value = value
...
>>> ls = [Foo(1), Foo(2), Foo(3)]
>>> min_value, max_value = reduce(lambda a, b: (a[0] if a[0].value < b.value else b, a[1] if a[1].value > b.value else b), ls, (ls[0], ls[1]))
>>> min_value
<__main__.Foo object at 0x10bd20940>
>>> min_value.value
1
>>> max_value.value
3

For what it's worth, though, I think it's a little clearer if you use a helper function. In this way it's easier to think cleanly about what your accumulator is (your Tuple) and how you're doing the comparison and using reduce().

from typing import Tuple
from functools import reduce


class Foo:
    def __init__(self, value): self.value = value

    def __repr__(self):
        return f"{self.value}"


def min_max(accumulator: Tuple[Foo, Foo], element: Foo) -> Tuple[Foo, Foo]:
    minimum, maximum = accumulator
    return (minimum if minimum.value < element.value else element,
            maximum if maximum.value > element.value else element)


ls = [Foo(x) for x in range(0, 4)]  # Or however you construct this list
minimum, maximum = reduce(min_max, ls, (ls[0], ls[0]))
print(f"{minimum=} {maximum=}")

Yielding:

minimum=0 maximum=3
Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102
0

You can define the __lt__ function, and then use min() and max() as you normally would:

from functools import reduce

class foo:
    def __init__(self,value):
        self.value = value
    
    def __lt__(self, other):
        return self.value < other.value 
    
x = []
x.append(foo(1))
x.append(foo(2))
x.append(foo(3))

min_value, max_value = min(x), max(x)
print(min_value.value, max_value.value)

This outputs:

1 3

This is a one-liner, but strictly speaking it's not one-pass. You can use reduce() for that. Other answerers have described methods that use reduce() as well, but defining __lt__ makes the syntax much cleaner, in my opinion:

min_value, max_value = reduce(lambda a, b: (min(a[0], b), max(a[1], b)), x, (x[0], x[0]))
print(min_value.value, max_value.value)
BrokenBenchmark
  • 18,126
  • 7
  • 21
  • 33
  • That counts as two passes, since `min` and `max` each consume the input separately. – chepner Apr 14 '22 at 17:18
  • This does return a correct result... but I don't think it's 'in one pass'. – Nathaniel Ford Apr 14 '22 at 17:18
  • Hmm, fair point. Let me think about this a bit more. I like defining the `__lt__` function because you can easily use it for other things: sorting algorithms, heaps, etc. – BrokenBenchmark Apr 14 '22 at 17:19
  • @NathanielFord -- Edited. Let me know what you think. – BrokenBenchmark Apr 14 '22 at 17:24
  • I think it's a worthwhile point that `__lt__` is useful - though I'm not entirely sure that making a min/max function call is cheaper or equivalently cheap as using inequalities. – Nathaniel Ford Apr 14 '22 at 17:28
  • The problem with defining `__lt__` is that you aren't necessarily picking one value or another. Given `(cur_min, cur_max)` and `x`, you aren't picking one or the other; you need to pick one of *three* values `(x, cur_max)`, `(cur_min, x)`, or `(cur_min, cur_max)`. – chepner Apr 14 '22 at 17:28