关于python:在现有属性递增时调用__setattr__?

Is __setattr__ called when an existing attribute is incremented?

现有属性递增时是否调用了uu setattr_uuuu?

我有一个叫做C的班级,我正试图超载。这是C类内部的代码部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class C:

    def bump(self):
        self.a += 1
        self.b += 1
        self.c += 1

    def __setattr__(self,name,value):
        calling = inspect.stack()[1]
        if 'private_' in name:
            raise NameError("\'private_\' is in the variable name.")
        elif '__init__' in calling.function:
            self.__dict__['private_' + name] = value
        elif name.replace('private_', '') in self.__dict__:
            if self.in_C(calling):
                if name.replace('private_', '') in self.__dict__.keys():
                    old_value = self.__dict__[name.replace('private_', '')]
                    new_value = old_value + value
                    self.__dict__[name.replace('private_', '')] = new_value
                else:
                    self.__dict__[name.replace('private_','')] = value
            else:
                raise NameError()
        else:
            self.__dict__[name] = value

_根据python文档,

object.__setattr__(self, name, value): Called when an attribute assignment is attempted. This is called instead of the normal mechanism (i.e. store the value in the instance dictionary). name is the attribute name, value is the value to be assigned to it.

我知道你可以给一个变量赋值(例如:C.__dict__[name] = value),但是当一个现有的属性递增时,比如bump()中的self.a += 1会怎么样?

假设属性A、B和C已经定义好了,我称之为bump(),然后称之为__setattr__。但是,我得到这个错误:

1
Error: o.bump() raised exception TypeError: unsupported operand type(s) for +=: 'NoneType' and 'int'

当现有属性递增时是否调用setattr?如果是,我将如何增加setattr中的现有属性?

注:假设在定义a、b和c之后调用bump()。另外,in_C(calling)是一个函数,用于检查__setattr__是从__init__调用的,还是从c内部调用的,或是从c外部调用的。

如果需要进一步澄清,请告诉我。


Python: Is __setattr__ called when an existing attribute is incremented?

答案是肯定的。使用简化版本的代码可以很容易地看到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
class C(object):

    def __init__(self, a):
        object.__setattr__(self, 'a', a)

    def __setattr__(self, name, value):
        print('Setting {} to {}'.format(name, value))
        object.__setattr__(self, name, value)


c = C(10)
c.a += 1

运行该代码段会产生:

1
Setting a to 11

您发布的代码的问题是,+=在调用__setattr__之前首先调用__getattribute__。如果属性不存在,这就是失败的原因。

解决方案是确保在调用bump()之前初始化属性:

1
2
3
4
5
6
class C(object):

    def __init__(self, a, b, c):
        object.__setattr__(self, 'a', a)
        object.__setattr__(self, 'b', a)
        object.__setattr__(self, 'c', a)

除此之外,还有其他错误(例如inspect中的错误),但这应该可以让您开始。


尽管看起来

1
a += 1

相当于a.__iadd__(1)(如果a__iadd__,实际上相当于:

1
a = a.__iadd__(1)    # (but `a` is only evaluated once.)

只是对于可变类型,__iadd__返回相同的对象,所以您看不到区别。

因此,如果目标是c.a,则调用c__setattr__。同样地,如果你做类似于c['a']+=1的事情,就称为__setitem__

这样做是因为python有不可变的类型,对于这些类型,一个增强的赋值将不做任何事情。

这一点已经记录在语法参考条目中,用于增加的作业(emphasis mine):

An augmented assignment evaluates the target (which, unlike normal
assignment statements, cannot be an unpacking) and the expression
list, performs the binary operation specific to the type of assignment
on the two operands, and assigns the result to the original target.
The target is only evaluated once.

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
In [44]: class C(object):
    ...:     def __init__(self,a):
    ...:         self.a=a
    ...:     def __setattr__(self,attr,value):
    ...:         print"setting `%s' to `%r'"%(attr,value)
    ...:         super(C,self).__setattr__(attr,value)
    ...:     def __setitem__(self,key,value):
    ...:         print"setitem"
    ...:         return setattr(self,key,value)
    ...:     def __getitem__(self,key):
    ...:         return getattr(self,key)
    ...:

In [45]: c=C([])
setting `a' to `[]'

In [46]: c['a']+=[1]
setitem
setting `a' to `[1]'

In [29]: class A(int):
    ...:     def __iadd__(s,v):
    ...:         print"__iadd__"
    ...:         return int.__add__(s,v)
    ...:     def __add__(s,v):
    ...:         print"__add__"
    ...:         return int.__add__(s,v)
    ...:

In [30]: c.a=A(10)
setting `a' to `10'

In [31]: c.a+=1
__iadd__
setting `a' to `11'