Python - 使用super重新实现__setattr__

4 投票
4 回答
8603 浏览
提问于 2025-04-17 21:34

我知道这个问题之前有人讨论过,可能不是最符合Python风格的类构建方式,但我有很多不同的Maya节点类,每个类都有很多@properties用来获取和设置节点数据。我想看看通过程序化的方式构建属性是否能减少开销和维护工作。

我需要重新实现__setattr__方法,以保持标准行为,但对于某些特殊属性,值需要设置为外部对象。

我在Stack Overflow上见过重新实现__setattr__的例子,但我似乎缺少了一些东西。

我觉得我没有保持setAttr的默认功能。

这里有一个例子:

externalData = {'translateX':1.0,'translateY':1.0,'translateZ':1.0}
attrKeys = ['translateX','translateY','translateZ']


class Transform(object):

    def __getattribute__(self, name):
        print 'Getting --->', name
        if name in attrKeys:
            return externalData[name]
        else:
            raise AttributeError("No attribute named [%s]" %name)

    def __setattr__(self, name, value):
        print 'Setting --->', name
        super(Transform, self).__setattr__(name, value)
        if name in attrKeys:
            externalData[name] = value


myInstance = Transform()
myInstance.translateX
# Result: 1.0 # 
myInstance.translateX = 9999
myInstance.translateX
# Result: 9999 # 
myInstance.name = 'myName'
myInstance.name
# AttributeError: No attribute named [name] #

!

4 个回答

0

为什么不在 __getattribute__ 里也做同样的事情呢?

    def __getattribute__(self, name):
        print 'Getting --->', name
        if name in attrKeys:
            return externalData[name]
        else:
            # raise AttributeError("No attribute named [%s]" %name)
            return super(Transform, self).__getattribute__(name)

测试代码

myInstance = Transform()
myInstance.translateX
print(externalData['translateX'])
myInstance.translateX = 9999
myInstance.translateX
print(externalData['translateX'])
myInstance.name = 'myName'
print myInstance.name
print myInstance.__dict__['name']

输出结果:

Getting ---> translateX
1.0
Setting ---> translateX
Getting ---> translateX
9999
Setting ---> name
Getting ---> name
myName
Getting ---> __dict__
myName
0

在你的代码片段中:

class Transform(object):

    def __getattribute__(self, name):
        print 'Getting --->', name
        if name in attrKeys:
            return externalData[name]
        else:
            raise AttributeError("No attribute named [%s]" %name)

    def __setattr__(self, name, value):
        print 'Setting --->', name
        super(Transform, self).__setattr__(name, value)
        if name in attrKeys:
            externalData[name] = value

看,在你的 __setattr__() 方法里,当你调用 myInstance.name = 'myName' 时,name 并不在 attrKeys 里面,所以它没有被放进 externalData 字典里,而是直接加到了 self.__dict__['name'] = value 里。

所以,当你试图查找这个特定的名字时,你并没有去 externalData 字典里找,因此你的 __getattribute__ 方法就会抛出一个异常。

你可以通过修改 __getattribute__ 来解决这个问题,具体的修改方式如下:

def __getattribute__(self, name):
    print 'Getting --->', name
    if name in attrKeys:
        return externalData[name]
    else:
        return object.__getattribute__(self, name)
2

我决定采用@theodox的方法,使用描述符。
这样做效果不错:

class Transform(object):

    def __init__(self, name):
        self.name = name
        for key in ['translateX','translateY','translateZ']:
            buildNodeAttr(self.__class__, '%s.%s' % (self.name, key))


def buildNodeAttr(cls, plug):
    setattr(cls, plug.split('.')[-1], AttrDescriptor(plug))


class AttrDescriptor(object):
    def __init__(self, plug):
        self.plug = plug

    def __get__(self, obj, objtype):
        return mc.getAttr(self.plug)

    def __set__(self, obj, val):
        mc.setAttr(self.plug, val)


myTransform = Transform(mc.createNode('transform', name = 'transformA'))
myTransform.translateX = 999

顺便提一下...
其实我原来的代码只需要把getattribute换成getattr就可以正常工作了。

不需要用到super。

3

这个方法对我有效:

class Transform(object):

    def __getattribute__(self, name):
       if name in attrKeys:
           return externalData[name]
       return super(Transform, self).__getattribute__(name)

    def __setattr__(self, name, value):
        if name in attrKeys:
            externalData[name] = value
        else:
            super(Transform, self).__setattr__(name, value)

不过,我不确定这是不是个好办法。

如果外部操作耗时较长(比如,你用这个来隐藏对数据库或配置文件的访问),可能会让使用这段代码的人产生错误的成本印象。在这种情况下,你应该使用一种方法,让用户明白他们是在发起一个操作,而不仅仅是在查看数据。

另一方面,如果访问速度很快,要小心不要破坏你类的封装性。如果你这样做是为了获取maya场景数据(像pymel那样,或者像这个例子),那就没什么大问题,因为时间成本和数据的稳定性基本上是有保障的。不过,你要避免你发布的示例代码中的情况:很容易就会假设设置了'translateX'为某个值后,它会保持不变,但实际上外部变量的内容可能会被改变,这样你就无法在使用这个类时知道你的不变条件。如果这个类是为了临时使用(比如,它是为了在一个没有其他操作的循环中进行快速重复处理的语法糖),那这样做是可以的——但如果不是,就要把数据内部化到你的实例中。

最后一个问题:如果你有“很多类”,你还需要做很多样板代码才能让这个工作。如果你想包装Maya场景数据,可以了解一下描述符(这里有个很棒的5分钟视频)。你可以像这样包装典型的变换属性:

import maya.cmds as cmds

class MayaProperty(object):
    '''
    in a real implmentation you'd want to support different value types, 
    etc by storing flags appropriate to different commands.... 
    '''
    def __init__(self, cmd, flag):
        self.Command = cmd
        self.Flag = flag

    def __get__(self, obj, objtype):
            return self.Command(obj, **{'q':True, self.Flag:True} )

    def __set__(self, obj, value):
        self.Command(obj, **{ self.Flag:value})

class XformWrapper(object):

    def __init__(self, obj):
        self.Object = obj

    def __repr__(self):
        return self.Object # so that the command will work on the string name of the object

    translation = MayaProperty(cmds.xform, 'translation')
    rotation = MayaProperty(cmds.xform, 'rotation')
    scale = MayaProperty(cmds.xform, 'scale')

在实际代码中,你需要处理错误和更清晰的配置,但你明白这个思路了。

上面链接的例子提到使用 metaclass 来填充类,当你有很多属性描述符需要配置时,这是一条不错的路。如果你不想担心所有的样板代码(不过它确实会有一点启动时间上的损失——我觉得这就是著名的Pymel启动缓慢的原因之一…)

撰写回答