Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to return a custom value for min and max in Python?

I have a custom class,

class A:
    def __init__(self, a, b):
        self.a = a
        self.b = b

The class is not iterable or indexable or anything like that. If at all possible, I would like to keep it that way. Is it possible to have something like the following work?

>>> x = A(1, 2)
>>> min(x)
1
>>> max(x)
2

What got me thinking about this is that min and max are listed as "Common Sequence Operations" in the docs. Since range is considered to be a sequence type by the very same docs, I was thinking that there must be some sort of optimization that is possible for range, and that perhaps I could take advantage of it.

Perhaps there is a magic method that I am not aware of that would enable this?

like image 339
Mad Physicist Avatar asked Jul 25 '17 15:07

Mad Physicist


People also ask

How do you find the minimum and maximum value in Python?

Use Python's min() and max() to find smallest and largest values in your data. Call min() and max() with a single iterable or with any number of regular arguments. Use min() and max() with strings and dictionaries.

How do you return a minimum value in Python?

The min() function returns the item with the lowest value, or the item with the lowest value in an iterable. If the values are strings, an alphabetically comparison is done.

How do you return a max value in Python?

Use max() to Find Max Value in a List of Strings and Dictionaries. The function max() also provides support for a list of strings and dictionary data types in Python. The function max() will return the largest element, ordered by alphabet, for a list of strings. The letter Z is the largest value, and A is the smallest.

What is the time complexity of MIN () and MAX () method in Python?

To find the maximum or minimum of a sequence, you must look at each element once, thus you can't get better than O(n). Of course, Python min and max have O(n) too: docs. You can write your own min/max function with a for loop and it will have the same complexity, but will be slower because it is not optimized in C.


3 Answers

Yes. When min takes one arguments it assumes it to be an iterable, iterates over it and takes the minimum value. So,

class A:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __iter__(self):
        yield self.a
        yield self.b

Should work.

Additional Note: If you don't want to use __iter__, I don't know of way to do that. You probably want to create your own min function, that calls some _min_ method if there is one in the argument it is passed to and calls the old min else.

oldmin = min
def min(*args):
    if len(args) == 1 and hasattr(args[0], '_min_'):
        return args[0]._min_()
    else:
        return oldmin(*args)
like image 140
Lærne Avatar answered Oct 09 '22 16:10

Lærne


There are no __min__ and __max__ special methods*. This is kind of a shame since range has seen some pretty nice optimizations in Python 3. You can do this:

>>> 1000000000000 in range(1000000000000)
False

But don't try this unless you want to wait a long time:

>>> max(range(1000000000000))

However creating your own min/max functions is a pretty good idea, as suggested by Lærne.

Here is how I would do it. UPDATE: removed the dunder name __min__ in favor of _min, as recommended by PEP 8:

Never invent such names; only use them as documented

Code:

from functools import wraps

oldmin = min

@wraps(oldmin)
def min(*args, **kwargs)
    try:
        v = oldmin(*args, **kwargs)
    except Exception as err:
        err = err
    try:
        arg, = args
        v = arg._min()
    except (AttributeError, ValueError):
        raise err
    try:
        return v
    except NameError:
        raise ValueError('Something weird happened.')

I think this way is maybe a little bit better because it handles some corner cases the other answer hasn't considered.

Note that an iterable object with a _min method will still be consumed by oldmin as per usual, but the return value is overridden by the special method.

HOWEVER, if the _min method requires the iterator to still be available for consumption, this will need to be tweaked because the iterator is getting consumed by oldmin first.

Note also that if the __min method is simply implemented by calling oldmin, things will still work fine (even though the iterator was consumed; this is because oldmin raises a ValueError in this case).

* Such methods are often called "magic", but this is not the preferred terminology.

like image 42
Rick supports Monica Avatar answered Oct 09 '22 17:10

Rick supports Monica


Since range is considered to be a sequence type by the very same docs, I was thinking that there must be some sort of optimization that is possible for range, and that perhaps I could take advantage of it.

There's no optimization going on for ranges and there are no specialized magic methods for min/max.

If you peek at the implementation for min/max you'll see that after some argument parsing is done, a call to iter(obj) (i.e obj.__iter__()) is made to grab an iterator:

it = PyObject_GetIter(v);
if (it == NULL) {
    return NULL;
}

then calls to next(it) (i.e it.__next__) are performed in a loop to grab values for comparisons:

while (( item = PyIter_Next(it) )) {
    /* Find min/max  */

Is it possible to have something like the following work?

No, if you want to use the built-in min* the only option you have is implementing the iterator protocol.


*By patching min, you can of-course, make it do anything you want. Obviously at the cost of operating in Pythonland. If, though, you think you can utilize some optimizations, I'd suggest you create a min method rather than re-defining the built-in min.

In addition, if you only have ints as instance variables and you don't mind a different call, you can always use vars to grab the instance.__dict__ and then supply it's .values() to min:

>>> x = A(20, 4)
>>> min(vars(x).values())
4
like image 41
Dimitris Fasarakis Hilliard Avatar answered Oct 09 '22 15:10

Dimitris Fasarakis Hilliard