Есть ли способ вернуть пользовательское значение min и max в Python?

У меня есть пользовательский класс,

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

Класс не является итерируемым или индексируемым или что-то в этом роде. Если это вообще возможно, я хотел бы сохранить его таким образом. Возможно ли иметь что-то вроде следующей работы?

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

Что заставило меня задуматься над тем, что min и max перечислены как "Операции общей последовательности" в docs. Поскольку range считается типом последовательности теми же документами, я думал, что должна быть какая-то оптимизация, которая возможна для range и, возможно, я мог бы воспользоваться этим.

Возможно, есть волшебный метод, о котором я не знаю, что бы это сделать?

Ответы

Ответ 1

Да. Когда min принимает один аргумент, он предполагает, что он итеративный, итерирует по нему и принимает минимальное значение. Так,

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

Должно сработать.

Дополнительное примечание: если вы не хотите использовать __iter__, я не знаю, как это сделать. Вы, вероятно, захотите создать свою собственную функцию min, которая вызывает некоторый метод __min__ если в аргументе он передается, и вызывает старый min еще.

oldmin = min
def min(*args):
    if len(args) == 1 and hasattr(args[0], '__min__'):
        return args[0].__min__()
    else:
        return oldmin(*args)

Ответ 2

Так как range считается типом последовательности теми же документами, я думал, что для range должна быть какая-то оптимизация, и, возможно, я смогу воспользоваться ею.

Там нет оптимизации для диапазонов и нет специальных методов магии для min/max.

Если вы заглянете в реализацию для min/max, вы увидите, что после того, как будет проведен синтаксический анализ аргументов, вызывается вызов iter(obj) (т.е. obj.__iter__()) для захвата итератора:

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

тогда вызовы next(it) (i.e it.__next__) выполняются в цикле для захвата значений для сравнения:

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

Возможно ли иметь что-то вроде следующей работы?

Нет, если вы хотите использовать встроенный min *, единственный вариант, который у вас есть, - это реализация протокола итератора.


* Исправляя min, вы можете, конечно, сделать все, что хотите. Очевидно, ценой работы в Pythonland. Если, однако, вы думаете, что можете использовать некоторые оптимизации, я бы предложил вам создать метод min, а не переопределять встроенный min.

Кроме того, если у вас есть только переменные типа int и вы не возражаете против другого вызова, вы всегда можете использовать vars для захвата instance.__dict__, а затем подайте его .values() в min:

>>> x = A(20, 4)
>>> min(vars(x).values())
4

Ответ 3

Нет __min__ и __max__ специальных методов *. Это позор, так как range видел некоторые довольно приятные оптимизации в Python 3. Вы можете сделать это:

>>> 1000000000000 in range(1000000000000)
False

Но не пробуйте это, если вы не хотите долго ждать:

>>> max(range(1000000000000))

Однако создание ваших собственных функций min/max - довольно хорошая идея, как предложено Lærne.

Вот как я это сделаю. UPDATE: удалено имя dunder __min__ в пользу _min, как рекомендовано PEP 8:

Никогда не изобретайте такие имена; используйте их только как документированные

код:

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.')

Я думаю, что этот способ, возможно, немного лучше, потому что он обрабатывает некоторые угловые случаи, которые другой ответ не учитывал.

Обратите внимание, что итерируемый объект с методом _min по-прежнему будет потребляться oldmin, как обычно, но возвращаемое значение переопределяется специальным методом.

ОДНАКО, если метод _min требует, чтобы итератор все еще был доступен для потребления, это нужно будет изменить, потому что итератор сначала потребляется oldmin.

Обратите также внимание, что если метод __min просто реализуется при вызове oldmin, все будет работать нормально (хотя итератор был использован, потому что oldmin вызывает в этом случае ValueError).

* Такие методы часто называют "волшебными", но это не предпочтительная терминология.