Как реализовать __iadd__ для свойства Python

Я пытаюсь создать свойство Python, в котором добавление на месте обрабатывается другим методом, чем получение значения, добавление другого значения и переназначение. Итак, для свойства x объекта o,

o.x += 5

должно работать иначе, чем

o.x = o.x + 5

Значение o.x в итоге должно быть одинаковым, чтобы не сбивать с толку ожидания людей, но я хочу сделать добавление на месте более эффективным. (На самом деле операция занимает гораздо больше времени, чем простое сложение.)

Моей первой идеей было определить в классе

x = property(etc. etc.)
x.__iadd__ = my_iadd

Но это вызывает AttributeError, предположительно потому, что property реализует __slots__?

Моя следующая попытка использует объект дескриптора:

class IAddProp(object):
    def __init__(self):
        self._val = 5

    def __get__(self, obj, type=None):
        return self._val

    def __set__(self, obj, value):
        self._val = value

    def __iadd__(self, value):
        print '__iadd__!'
        self._val += value
        return self

class TestObj(object):
    x = IAddProp()
    #x.__iadd__ = IAddProp.__iadd__  # doesn't help

>>> o = TestObj()
>>> print o.x
5
>>> o.x = 10
>>> print o.x
10
>>> o.x += 5  # '__iadd__!' not printed
>>> print o.x
15

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

Как я могу это сделать? Я не понимаю смысла дескрипторов? Нужен ли мне метакласс?


person ptomato    schedule 16.08.2012    source источник


Ответы (4)


__iadd__ будет искаться только в значении, возвращенном из __get__. Вам нужно заставить __get__ (или метод получения свойства) возвращать объект (или прокси-объект) с помощью __iadd__.

@property
def x(self):
    proxy = IProxy(self._x)
    proxy.parent = self
    return proxy

class IProxy(int, object):
    def __iadd__(self, val):
        self.parent.iadd_x(val)
        return self.parent.x
person ecatmur    schedule 16.08.2012
comment
Меня немного смущает этот фрагмент кода. Разве получатель свойства не должен что-то возвращать? - person senderle; 16.08.2012
comment
@senderle о да, ой. Немного запутался с проблемами создания подклассов int. Спасибо! - person ecatmur; 16.08.2012

Оператор += в строке

o.x += 5

переводится на

o.x = o.x.__iadd__(5)

Поиск атрибута с правой стороны переводится в

o.x = IAddProp.__get__(TestObj2.x, o, TestObj2).__iadd__(5)

Как видите, __iadd__() вызывается для возвращаемого значения поиска атрибута, поэтому вам нужно реализовать __iadd__() для возвращаемого объекта.

person Sven Marnach    schedule 16.08.2012
comment
Это неправильно. Либо вызывается o.x.__iadd__(5), либо, если o.x не имеет метода и __iadd__, вызывается o.x = o.x + 5. То есть o.x = o.x.__iadd__(5) никогда не используется, потому что __iadd__ ничего не возвращает. - person Mark Lodato; 28.08.2013
comment
@MarkLodato Пожалуйста, проверьте свои утверждения, прежде чем выкрикивать их. Это точно так же, как weitten в ответе. Попробуйте. - person glglgl; 11.12.2013
comment
@MarkLodato Посмотрите здесь: codepad.org/wbURpQJT Здесь показано, как __iadd__() вызывается при выполнении += (способность оценивать что-либо совершенно новый для левого имени), а также показывает, что возвращает list.__iadd__(): это self. - person glglgl; 11.12.2013
comment
@glglgl, ты прав. Извини за это. Ссылка: docs.python.org/3/reference/ - person Mark Lodato; 12.12.2013

Почему бы не что-то вроде следующего примера. В основном идея состоит в том, чтобы позволить классу Bar гарантировать, что сохраненное значение для свойства x всегда является объектом Foo.

class Foo(object):
    def __init__(self, val=0):
        print 'init'
        self._val = val

    def __add__(self, x):
        print 'add'
        return Foo(self._val + x)

    def __iadd__(self, x):
        print 'iadd'
        self._val += x
        return self

    def __unicode__(self):
        return unicode(self._val)

class Bar(object):
    def __init__(self):
        self._x = Foo()

    def getx(self):
        print 'getx'
        return self._x

    def setx(self, val):
        if not isinstance(val, Foo):
            val = Foo(val)
        print 'setx'
        self._x = val

    x = property(getx, setx)

obj = Bar()
print unicode(obj.x)
obj.x += 5
obj.x = obj.x + 6
print unicode(obj.x)

РЕДАКТИРОВАТЬ: Расширенный пример, чтобы показать, как использовать его в качестве свойства. Сначала я немного неправильно понял проблему.

person muksie    schedule 16.08.2012

Чтобы вдохновить вас, вот далеко не идеальное решение, лучшее из того, что мне удалось придумать:

class IAddObj(object):
    def __init__(self):
        self._val = 5

    def __str__(self):
        return str(self._val)

    def __iadd__(self, value):
        print '__iadd__!'
        self._val += value
        return self._val

class TestObj2(object):
    def __init__(self):
        self._x = IAddObj()

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x._val = value

>>> o = TestObj2()
>>> print o.x
5
>>> o.x = 10
>>> print o.x
10
>>> o.x += 5
__iadd__!
>>> print o.x
15
>>> print o.x + 5  # doesn't work unless __add__ is also implemented
TypeError: unsupported operand type(s) for +: 'IAddObj' and 'int'

Большим недостатком является то, что вам нужно реализовать полный набор числовых магических методов для IAddObj, если вы хотите, чтобы свойство вело себя как число. Наследование IAddObj от int, похоже, также не позволяет ему наследовать операторы.

person ptomato    schedule 16.08.2012
comment
Вы говорите, что наследование IAddObj от int, похоже, также не позволяет ему наследовать операторы. Это не совсем так. Проблема в том, что если вы не переопределяете все остальные операторы, то тип IAddObj(5) + 5 будет int, а не IAddObj, потому что целые числа неизменяемы, и все операции всегда возвращают новые целые числа. Однако это действительно можно обойти с помощью метаклассов. см. этот ответ. - person senderle; 16.08.2012
comment
Однако, подумав об этом подробнее, я считаю, что ecatmur и muksie предлагают решения, которые будут менее сложными и более подходящими для ваших реальных целей. - person senderle; 16.08.2012