2

I'm currently trying to code an equivalent for the built-in min-max function in python, and my code return a pretty weird exception which I don't understand at all:

TypeError: 'generator' object is not subscriptable, min, 7, , 9

when i try it with:

min(abs(i) for i in range(-10, 10))

Here is my code:

def min(*args, **kwargs):
key = kwargs.get("key", None)
argv=0
for i in args:
    argv+=1
    if argv == 1 and (type(args) is list or type(args) is tuple or type(args) is str):
        min=args[0][0]
        for i in args[0]:
            if key != None:
                if key(i) < key(min):
                    min = i
            else:
                if i < min:
                    min = i
        return min
    else:
        min=args[0]
        for i in args:
            if key != None:
                if key(i) < key(min):
                    min = i
            else:
                if i < min:
                    min = i
        return min

According to the documentation, i should be able to iterate over a generator...

jehutyy
  • 364
  • 3
  • 11
  • 1
    You can *iterate* over generators. You can't do `generator[0]` ("not subscriptable"). It complains about `min=args[0][0]`. – hlt Aug 11 '14 at 18:07
  • But how can I set a default value to min if i can't pick it up in its values ? – jehutyy Aug 11 '14 at 18:22
  • Set it to `None` first and then check if it is `None` when iterating over the generator (so you can set it to the first element) and afterwards (if there are no elements). – hlt Aug 11 '14 at 18:23
  • Just so you know, `args` will _always_ be a tuple, so your `if` block will always be entered, and your `else` block will never be entered. Furthermore, since both `if` and `else` have an unconditional `return` inside them, your first `for` loop will never get past the first iteration. You may as well remove it, and the `argv` check as well. – Kevin Aug 11 '14 at 18:25
  • ok thank you, i didn't know, i'll try hlt solution and change my code, – jehutyy Aug 11 '14 at 18:27

3 Answers3

3

Here is my implementation:

def max(*args, **kwargs):
    key = kwargs.get("key", lambda x: x)
    if len(args) == 1:
        args = args[0]
    maxi = None
    for i in args:
        if maxi == None or key(i) > key(maxi):
            maxi = i
    return maxi

def min(*args, **kwargs):
    key = kwargs.get("key", lambda x: x)
    if len(args) == 1:
        args = args[0]
    mini = None
    for i in args:
        if mini == None or key(i) < key(mini):
            mini = i
    return mini

A little bit more concise than preview post.

2

The issue you are having is due to the fact that min has two function signatures. From its docstring:

min(...)
    min(iterable[, key=func]) -> value
    min(a, b, c, ...[, key=func]) -> value

So, it will accept either a single positional argument (an iterable, who's values you need to compare) or several positional arguments which are the values themselves. I think you need to test which mode you're in at the start of your function. It is pretty easy to turn the one argument version into the multiple argument version simply by doing args = args[0].

Here's my attempt to implement the function. key is a keyword-only argument, since it appears after *args.

def min(*args, key=None):    # args is a tuple of the positional arguments initially
    if len(args) == 1:       # if there's just one, assume it's an iterable of values
        args = args[0]       # replace args with the iterable

    it = iter(args)          # get an iterator

    try:
        min_val = next(it)   # take the first value from the iterator
    except StopIteration:
        raise ValueError("min() called with no values")

    if key is None:       # separate loops for key=None and otherwise, for efficiency
        for val in it:    # loop on the iterator, which has already yielded one value
            if val < min_val
                min_val = val
    else:
        min_keyval = key(min_val)    # initialize the minimum keyval
        for val in it:
            keyval = key(val)
            if keyval < min_keyval:  # compare keyvals, rather than regular values
                min_val = val
                min_keyval = keyval

    return min_val

Here's some testing:

>>> min([4, 5, 3, 2])
2
>>> min([1, 4, 5, 3, 2])
1
>>> min(4, 5, 3, 2)
2
>>> min(4, 5, 3, 2, 1)
1
>>> min(4, 5, 3, 2, key=lambda x: -x)
5
>>> min(4, -5, 3, -2, key=abs)
-2
>>> min(abs(i) for i in range(-10, 10))
0
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • It is a pretty elegant way to do it, I must admit i wouldn't have think to it this way. But i don't understand why you create an iterator with iter(args)... args is already an iterator isn't it ? – jehutyy Aug 13 '14 at 19:54
0

Functions in question have a lot in common. In fact, the only difference is comparison (< vs >). In the light of this fact we can implement generic function for finding and element, which will use comparison function passed as an argument. The min and max example might look as follows:

def lessThan(val1, val2):
  return val1 < val2

def greaterThan(val1, val2):
  return val1 > val2


def find(cmp, *args, **kwargs):
  if len(args) < 1:
    return None

  key = kwargs.get("key", lambda x: x)
  arguments = list(args[0]) if len(args) == 1 else args
  result = arguments[0]

  for val in arguments:
    if cmp(key(val), key(result)):
      result = val

  return result


min = lambda *args, **kwargs: find(lessThan, *args, **kwargs)
max = lambda *args, **kwargs: find(greaterThan, *args, **kwargs)

Some tests:

>>> min(3, 2)
2
>>> max(3, 2)
3
>>> max([1, 2, 0, 3, 4])
4
>>> min("hello")
'e'
>>> max(2.2, 5.6, 5.9, key=int)
5.6
>>> min([[1, 2], [3, 4], [9, 0]], key=lambda x: x[1])
[9, 0]
>>> min((9,))
9
>>> max(range(6))
5
>>> min(abs(i) for i in range(-10, 10))
0
>>> max([1, 2, 3], [5, 6], [7], [0, 0, 0, 1])
[7]
Wiktor Czajkowski
  • 1,623
  • 1
  • 14
  • 20