Python:类属性/变量的多态继承?

7 投票
2 回答
3002 浏览
提问于 2025-04-17 08:42

作为一个学习Python的初学者,我最近在处理类属性时遇到了一些奇怪的(在我看来)行为。我并不是在抱怨,但希望能得到一些有用的评论,帮助我理解这个问题。

为了把复杂的问题简化成一个更简洁的提问,我想这样问:

在Python中,如何确保一个类属性在继承关系中更像一个静态变量?

我感觉类属性的行为就像是一个“读取时复制”的默认值,并且具有多态特性。只要我进行“只读”操作,它就保持为一个“单例”,但一旦我通过派生类或实例对类属性进行赋值,它就会变成一个新的引用,失去与继承的基类引用的关系。

(这确实有一些有趣的特性,但你必须理解它才能接受它,所以我非常希望能得到一些见解。)

class A(object):
    classvar = 'A'
    def setclassvar(self, value):
        A.classvar = value                   
    def __str__(self):
        return "%s id(%s) " %(A.classvar, hex(id(A.classvar))[2:-1].upper())

class A1(A):
    pass

class B(object):
    classvar = 'B'
    def setclassvar(self, value):
        self.__class__.classvar = value            
    def __str__(self):
        cvar = self.__class__.classvar
        return "%s id(%s) " %(cvar, hex(id(cvar))[2:-1].upper())

class B1(B):
    def setclassvar(self, value):
        self.__class__.classvar = value

a, a1 = A(), A1()
a1.setclassvar('a')
print "new instance A: %s" %a
print "new instance A1: %s" %a

b, b1 = B(), B1()
b1.setclassvar('bb')
print "new instance B: %s" %b
print "new instance B1: %s" %b1

a1.setclassvar('aa')
print "new value a1: %s" %a
print "new value a: %s" %a

a1.classvar = 'aaa'
print "direct access a1: %s id(%s)" %(a1.classvar, hex(id(a1.classvar))[2:-1].upper())
print "method access a1: %s" %a1
print "direct access a: %s" %a

这段代码产生了以下结果:

new instance A: a id(B73468A0) 
new instance A1: a id(B73468A0) 
new instance B: B id(B73551C0) 
new instance B1: bb id(AD1BFC) 
new value a1: aa id(AD1BE6) 
new value a: aa id(AD1BE6) 
direct access a1: aaa id(A3A494)
method access a1: aa id(AD1BE6) 
direct access a: aa id(AD1BE6)

所以直接访问(赋值)object.classvar或者通过self.__class__.classvar访问的结果都和BASECLASS.classvar不一样。

这是作用域的问题,还是完全不同的原因呢?

期待你们的回答,提前谢谢大家。:-)


补充:之前有一个答案提到使用类描述符,比如:如何创建类属性?

不幸的是,这似乎不奏效:

class Hotel(Bar):
    def __init__(self):        
        Hotel.bar += 1

hotel = Hotel()
assert hotel.bar == 51
assert hotel.bar == foo.bar

第二个断言失败了!hotel.bar并不引用与foo.bar相同的对象,而hotel.bar引用的东西与Hotel.bar也不同!


第二次补充:我很清楚单例被认为是一种“反模式”,我并不打算广泛使用它们。因此我在问题标题中没有提到它们。尽管有很多讨论和提供关于单例的解决方案,我的问题依然是:为什么一个类变量可以如此轻易地断开它的引用?Ruby的表现方式更符合我的直觉:http://snippets.dzone.com/posts/show/6649

2 个回答

3
a1.classvar = 'aaa'

不是对类变量的“引用”。

这是在对象'a1'中创建了一个新的实例变量。

A.classvar这样的表达式是类变量。类对象(还有它的父类)都有一个类级别的字典(A.__dict__),里面定义了类级别的对象。名字解析是通过先检查类,然后检查所有的父类,按照方法解析顺序(MRO)来进行的。

a.classvar这样的表达式是通过搜索对象的命名空间来解析的。当这是一个“读取”引用时,会搜索对象和类(以及父类)。

当这个出现在赋值的左边时,实例变量(“classvar”)只是简单地在引用的对象(“a”)上创建。因为没有需要解析的名字,所以不需要在父命名空间中搜索。它就是在被创建。

1

如果实现起来很难解释,那就是个坏主意。

在这种情况下,我会提供一个实现示例,主要是为了完整性,以及因为这是我在Python中喜欢的那种棘手的东西。

因此,下面的代码片段有点利用了闭包的特性,来创建一个类装饰器,满足提问者的需求:一个类变量在派生类中读取和写入时保持“统一”。

而且,作为额外的好处,我把这个属性放在一个描述符中,这样在实例中这个属性就变得不可更改了——所以无论是在子类还是子类的实例中写入这个属性时,类属性都会被正确更新。

正如Python的哲学所说:“如果实现起来很难解释,那就是个坏主意”——我觉得我想不出比这更复杂的了——我们在这里讨论的是作用域动态生成的元类。这确实能工作,但这段代码失去了“Pythonic”的特性,因为它由于大量使用类、元类、闭包和描述符机制而变得非常晦涩。

def SingletonAttrs(**names):
    keys = names.keys()
    def class_decorator(cls):
        class Meta(type):
            def __getattribute__(cls, attr):
                if attr in keys:
                    return type.__getattribute__(owner_cls,  attr)
                return type.__getattribute__(cls, attr)
            def __setattr__(cls, attr, value):
                if attr in keys:
                    class Wrapper(object):
                        def __init__(self, value):
                            self.__set__(None, value)
                        __set__ = lambda self, instance, value: setattr(owner_cls,"__" +  attr, value)
                        __get__ = lambda self, instance, owner: type.__getattribute__(owner_cls, "__" + attr)
                    return type.__setattr__(owner_cls,  attr, Wrapper(value))
                return type.__setattr__(cls, attr, value)
        owner_cls = Meta(cls.__name__, cls.__bases__, cls.__dict__.copy())
        for key in keys:
            setattr(owner_cls, key, names[key])
        return owner_cls
    return class_decorator

if __name__ == "__main__":

    @SingletonAttrs(a="value 1", b="value 2")
    class Test(object):
        pass

    class TestB(Test):
        pass

    t = Test()
    print t.a
    print t.b
    tb = TestB()
    tb.a = "value 3"
    print Test.a

撰写回答