Как обезвреживать методы dunder для существующих экземпляров?

Контекст: я хотел бы использовать heapq (и что-нибудь еще) для объектов, которые я не создавал, которые сами по себе не имеют оператора __lt__. Могу я? (без класса-оболочки).

класс:

class Node:
    def __init__(self, val):
        self.val = val

Теперь во время выполнения в интерпретаторе мне вручается некоторая коллекция объектов. Я хочу перебрать их, добавив метод dunder (в моем случае lt), например:

n = Node(4)
m = Node(5)

def myLT(self, other):
    return self.val < other.val

Что я пробовал:

n.__lt__ = types.MethodType(myLT, n)
m.__lt__ = types.MethodType(myLT, m)

также

n.__lt__ = types.MethodType(myLT, n)
m.__lt__ = types.MethodType(myLT, n)

(на случай, если привязка одного и того же функтора улучшит ситуацию)

>>> n < m
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Node' and 'Node'

даже не смотря на:

>>> n.__lt__(m)
True

Я могу использовать класс-оболочку, который в некотором смысле неприятный (дополнительная память и код обхода становятся более уродливыми, но, по крайней мере, не затрагивают исходные объекты):

class NodeWrapper:
    def __init__(self, n):
        self.node = n
    def __lt__(self):
        return self.node.val

Мне просто интересно узнать, делаю ли я что-то не так, добавляя метод dunder, или это просто не работает в python 3.x. Я использую 3.6.9, если это имеет значение.


person wheelreinventor    schedule 26.12.2020    source источник
comment
Обертка, вероятно, то, что вы хотите, честно. Если вы хотите уменьшить требования к памяти, используйте __slots__.   -  person juanpa.arrivillaga    schedule 26.12.2020


Ответы (1)


Вы можете попробовать исправить дандер, изменив свойство __class__ экземпляра. Как поясняется в разделе документации Поиск специального метода:

Для пользовательских классов неявные вызовы специальных методов гарантированно работают правильно только в том случае, если они определены для типа объекта, а не в словаре экземпляра объекта.


def patch_call(instance, func, memo={}):
    if type(instance) not in memo:
        class _(type(instance)):
            def __lt__(self, *arg, **kwargs):
               return func(self, *arg, **kwargs)
        memo[type(instance)] = _

    instance.__class__ = memo[type(instance)]

patch_call(m, myLT)
patch_call(n, myLT)

n < m
# True

Изменено по ссылке.

Спасибо @juanpa.arrivilaga за рекомендацию кэшировать классы для повышения производительности.

person cs95    schedule 26.12.2020
comment
Вы, должно быть, делаете что-то другое, или я неправильно понял. Я попробовал вышеописанное и получил: TypeError: myLT() missing 1 required positional argument: 'other' на onlinegdb - person wheelreinventor; 26.12.2020
comment
@wheelreinventor Это была ошибка копирования и вставки в моем коде, вы сделаете еще одну попытку сейчас? - person cs95; 26.12.2020
comment
Я бы кэшировал на основе type(instance), иначе вы создаете класс для каждого экземпляра. Что может быть невероятно расточительным - person juanpa.arrivillaga; 26.12.2020
comment
Между прочим, я наблюдал это экспериментально, исправление обезьяны с использованием этого метода примерно в два раза медленнее и на ~ 25% больше памяти с учетом параметров по сравнению с классом-оболочкой. - person wheelreinventor; 26.12.2020
comment
@ juanpa.arrivillaga интересно, это, кажется, решает проблему производительности, только что упомянутую OP. Как будет кэшироваться результат? Используете functools.lru_cache? - person cs95; 26.12.2020
comment
ну, вам нужно будет передать тип в качестве аргумента, если вы хотите использовать lru_cache, но вы можете просто сохранить dict, что-то вроде seen_types, затем if type(instance) in seen_types: _ = seen_types[type(instance)] else: ... - person juanpa.arrivillaga; 26.12.2020
comment
Ребята, отличные комментарии, спасибо! @wheelreinventor Я приглашаю вас взглянуть на пересмотренный ответ, основанный на комментарии juanpa о кэшировании класса. - person cs95; 26.12.2020