类属性与实例属性

Python 小记 2018-12-15 4674 字 86 浏览 点赞

类属性

同一个类的多个实例共用一个类属性。

import random

class MyClass(object):
    # 类属性
    num = random.randrange(10000)

if __name__ == "__main__":
    myObjOne = MyClass()
    myObjTwo = MyClass()

    print(myObjOne.num == myObjTwo.num)

可以看到输出结果恒为True。

实例属性

同一个类的多个实例拥有各自的实例属性。

import random

class MyClass(object):
    def __init__(self):
        self.num = random.randrange(10000)

if __name__ == "__main__":
    myObjOne = MyClass()
    myObjTwo = MyClass()

    print(myObjOne.num == myObjTwo.num)

大概率下,输出结果为False。

类属性的陷阱

当我们想改变类属性的时候:

class MyClass(object):
    num = 512

if __name__ == "__main__":
    myObj = MyClass()
    print(myObj.num, myObj.__class__.num)  # 通过实例访问类属性和类层次访问类属性  输出:512, 512
    myObj.num = 1024  # 企图通过实例改变类属性
    print(myObj.num, myObj.__class__.num)  # 输出:1024, 512

可以看到,尽管解释器没有抛异常,但我们并没有改变类属性的值。同时,我们似乎为实例myObj增加了一个实例属性,它与类属性具有相同的名称——num。到底是不是这样呢?可以打印实例的__dict__属性

print(myObj.__dict__)  # 输出:{}
myObj.num = 1024
print(myObj.__dict__)  # 输出:{'num': 1024}

也就是说,实例不可以 直接 修改类层次的属性。并且当实例属性与类属性同名时,实例属性会屏蔽掉类属性。

访问属性的优先级顺序

用“屏蔽”一词确实含糊地解释了上述现象,但究其根本是Python中存在默认的一套访问顺序。

dercls = DeriveClass(),也就说dercls是一个实例对象时,dercls.num会去类层次找num或者基类中找num:

class BaseClass(object):
    num = 512

class DeriveClass(BaseClass):
    pass

if __name__ == "__main__":
    dercls = DeriveClass()
    print(dercls.num)  # 输出:512
class BaseClass(object):
    num = 512

class DeriveClass(BaseClass):
    num = 1024

if __name__ == "__main__":
    dercls = DeriveClass()
    print(dercls.num)  # 输出: 1024

可见实例对象类的优先级 > 基类

class BaseClass(object):
    num = 512

class DeriveClass(BaseClass):
    num = 1024
    
    def __init__(self):
        # 实例属性也同名
        self.num = 2048

if __name__ == "__main__":
    dercls = DeriveClass()
    print(dercls.num)  # 输出: 2048

所以实例属性优先级 > 实例该类的类属性

而这个查找属性的过程是在__getattribute__中完成的,当__getattribute__找不到需要的属性时,会抛出AttributeError,此时解释器会自动调用__getattr__,仍然找不到,则抛异常AttribueError并且挂掉程序

class BaseClass(object):
    def __getattribute__(self, item):
        if item == "num":
            return 512

    def __getattr__(self, item):
        if item == "num":
            return 1024

if __name__ == "__main__":
    basecls = BaseClass()
    print(basecls.num)  # 输出: 512
class BaseClass(object):
    def __getattribute__(self, item):
        raise AttributeError

    def __getattr__(self, item):
        if item == "num":
            return 1024

if __name__ == "__main__":
    basecls = BaseClass()
    print(basecls.num)  # 输出: 1024

得到:通过_getattribute__查找属性的优先级 > 通过_getattr

但通过__getattribute__查找属性并不只有类属性和实例属性,事实上查找描述符也是在__getattribute__中完成的。此时分资料描述符非资料描述符

资料描述符:

class IntegerField(object):
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        pass

class BaseClass(object):
    num = IntegerField(2048)  # 这是一个资料描述符

    def __init__(self):
        self.num = 512

    def __getattr__(self, item):
        if item == "num":
            return 1024

if __name__ == "__main__":
    basecls = BaseClass()
    print(basecls.num)  # 输出:2048

非资料描述符:

class IntegerField(object):
    def __init__(self, value):
        self.value = value

    def __get__(self, instance, owner):
        return self.value

class BaseClass(object):
    num = IntegerField(2048)  # 这是一个非资料描述符

    def __init__(self):
        self.num = 512

    def __getattr__(self, item):
        if item == "num":
            return 1024

if __name__ == "__main__":
    basecls = BaseClass()
    print(basecls.num)  # 输出: 512

(关于描述符,你可以参看我的属性描述符

总结

如果user是某个类的实例,那么user.age(以及等价的getattr(user, 'age'))首先调用__getattribute__。如果类定义了__getattr__方法。那么在__getattribute__抛出AttributeError的时候就会调用__getattr__

描述符的调用(__get__)发生在__getattribute__内部。user = User(),那么user.age顺序如下:

  • 如果age出现在User或其基类的__dict__中,且age是data descriptor,那么调用__get__
  • 如果age出现在obj的__dict__中,那么直接返回obj.__dict__["age"]
  • 如果age出现在User或其基类的__dict__中:
  1. 如果age是non-data descriptor,那么调用其__get__方法;否则返回__dict__["age"]
  2. 如果User有__getattr__方法,在__getattribute__AttributeError之后调用其__getattr__;否则抛出AttributeError并挂掉程序

感谢



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论