如何为 Python 属性实现 __iadd__?

15
我将尝试创建一个Python属性,其中就地添加由不同的方法处理,而检索值、添加另一个值并重新分配则由另一种方法处理。因此,对于对象o上的属性x
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__绕过了它。

我该怎么做呢?我没有理解描述符的要点吗?我需要一个元类吗?

4个回答

9

__iadd__ 只会在从 __get__ 返回的值上查找。你需要让 __get__ (或属性 getter) 返回一个具有 __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

我对这段代码有点困惑。属性的getter方法不应该返回一些东西吗? - senderle
@senderle 哦,是的,不好意思。子类化“int”的问题有点令人困惑。谢谢! - ecatmur

7
在这行代码中,+= 操作符表示
o.x += 5

被翻译为

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

右侧的属性查找将被翻译为
o.x = IAddProp.__get__(TestObj2.x, o, TestObj2).__iadd__(5)

正如您所看到的,__iadd__() 是在属性查找的返回值上调用的,因此您需要在返回的对象上实现__iadd__()


这是不正确的。要么调用o.x.__iadd__(5),要么如果o.x没有__iadd__方法,则调用o.x = o.x + 5。也就是说,永远不会使用o.x = o.x.__iadd__(5),因为__iadd__不返回任何东西。 - Mark Lodato
@MarkLodato在发出声音之前,请验证您的说法。回答中正是如此写的。试一试吧。 - glglgl
@MarkLodato 看这里:http://codepad.org/wbURpQJT 它展示了当使用 += 时如何调用 __iadd__()(能够将完全新的内容赋值给左手边的名称),它也展示了 list.__iadd__() 返回什么:即它的 self - glglgl
@glglgl,你是对的。对此我很抱歉。参考链接:http://docs.python.org/3/reference/datamodel.html#emulating-numeric-types - Mark Lodato

3
为什么不采用以下示例呢?基本上,这个想法是让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)

编辑:扩展示例以展示如何将其用作属性。我稍微误解了问题。


1
为了激励你,这里有一个不太理想的解决方案,但这是我目前能想到的最好的方案。
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 似乎也无法继承运算符。


你说“让IAddObj从'int'继承似乎也无法继承运算符。”这并不完全正确。问题在于,如果您没有覆盖所有其他运算符,则'IAddObj(5)+ 5'的类型为'int'而不是'IAddObj',因为int是不可变的,并且所有操作始终返回新的int。但是,确实有一种使用元类解决此问题的方法。[请参见此答案](https://dev59.com/QGgv5IYBdhLWcg3wNeHS)。 - senderle
不过仔细想想,我认为ecatmur和muksie提供的解决方案会更简单,更适合您的实际目标。 - senderle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接