python 属性查询顺序,数据描述符

数据描述符,属性查找优先级

如果在一个类中定义了 __get__() , __set__(), __delete__() 这三种方法之一,那么这个类是一个描述符。

描述符分成两种:

  • 如果这种类只定义了 __get__ 方法,那么就是一个非数据描述符,
  • 定义了 __get__()__set__() 的数据描述符。

描述符的用处就是,当一个对象的某个属性是一个描述符时,你访问这个描述符类型的属性,就会调用这个描述符的方法。譬如你获取描述符的值时,会调用它的__get__().

我们先看一下这三个方法的docstring:

    def __delete__(self, *args, **kwargs): # real signature unknown
        """ Delete an attribute of instance. """
        # 删除一个实例的属性
    
    def __set__(self, *args, **kwargs): # real signature unknown
        """ Set an attribute of instance to value. """
        # 给实例的属性设置一个值

    def __get__(self, *args, **kwargs): # real signature unknown
        """ Return an attribute of instance, which is of type owner. """
        # 返回实例的属性,该实例是 `owner` 类型的

实例:

class A(object):
    def __init__(self):
        self.value = None

    def __set__(self, instance, value): # self:类A的实例,也是类B的属性a;instance:类 B 的实例 b;value:通过b.a的赋值
        print('set: self,instance,value',self,instance,value)
        self.value = value
        return self.value

    def __get__(self, instance, owner):# instance:类B的实例b;owner:类B
        print('get: self,instance,owner',self,instance,owner)
        return self.value

class B(object):
    a = A()

    def __init__(self):
        self.val = 20

  1. 上述代码中,有两个类,AB。先看类 B,有一个类属性 a , 且 a 是类 A 的实例,我们先来实例化一下类 B ,看一下 类 B 和实例 b 的属性:
b = B()
print(b.__dict__)
print(B.__dict__)

"""
{'val': 20}

{'__module__': '__main__', 'a': <__main__.A object at 0x0163FD70>, '__init__': <function B.__init__ at 0x07845078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
"""

可以看出,实例 b 的属性中,只有一个 val 属性 ;类 B 的属性中,则有一个 a ,且 a 是类 A 的一个对象。

  1. 接下来,我们调用一下实例 a
b = B()
b.a
B.a

"""
get: self,instance,owner <__main__.A object at 0x03458E68> <__main__.B object at 0x03458F28> <class '__main__.B'>

 get: self,instance,owner <__main__.A object at 0x03458E68> None <class '__main__.B'>
"""

我们看一下什么意思:

  1. 当调用 b.a 时,程序会自动去调用 b.__getattribute__('a') , 也就是 b.__dict__['a'], 即通过对象 b 的字典去查找属性,但是在第一步我们已经知道, 对象 b 只有一个属性 {'val': 20} ,既然在实例 b 中找不到 a。 所以会去父类中找,调用:type(b).__dict__['a'].__get__(b,type(b)), 也就是: B.__dict__['a'].__get__(b,B),打印了第一行的信息,并返回了None

  2. 当调用 B.a 时,会直接调用 B.__dict__['a'].__get__(None,B),所以第二处打印的信息中间有个 None

  3. 现在,我们尝试给 b.a 赋值

b = B()
b.a = 11
print(b.__dict__)
print(B.__dict__)
B.a = 12
print(b.__dict__)
print(B.__dict__)

"""
set: self,instance,value <__main__.A object at 0x037CFD70> <__main__.B object at 0x037CFDD0> 11
{'val': 20}

{'__module__': '__main__', 'a': <__main__.A object at 0x037CFD70>, '__init__': <function B.__init__ at 0x07E85078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}

{'val': 20}

{'__module__': '__main__', 'a': 12, '__init__': <function B.__init__ at 0x07E85078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
"""

可以看出,当调用了 b.a=11 时,调用了描述符的 __set__() , 但是对象 b 的实例属性并没有改变,依然只有 val=20, 同时类B的类属性也没有改变。 但是当调用 B.a = 12 时,类属性 a 变成了12,并没有调用描述符的 __set__() 方法。

所以,结合上面的 docstring,我们可以看出,数据描述符应该是给实例使用的,类使用它用处不大,至少没法调用它的 __set__()

  1. 如果类属性的描述符对象和实例对象的属性同名,如果查找?

    也就是说,如果把类B改成:

class B(object):
	a = A()

	def __init__(self):
		self.val = 20
		self.a = 11  # 这里同名的属性

此时调用 b.a ,会如何?

  1. 当类A是一个数据描述符,也就是说类A包含 __set__ 方法时,此时数据描述符优先级高,所以实例属性 self.a 其实就是对类属性 a 的赋值,会调用数据描述符的 __set__ 方法:
set: self,instance,value <__main__.A object at 0x009DFD70> <__main__.B object at 0x009DFDD0> 11
get: self,instance,owner <__main__.A object at 0x009DFD70> <__main__.B object at 0x009DFDD0> <class '__main__.B'>
11
  1. 当类A是一个非数据描述符,那么实例的字典优先级高,所以会使用实例字典中的数据,即结果:
11

属性查询优先级:

  1. obj.__getattribute__()

  2. 数据描述符

  3. 实例的字典

  4. 类的字典

  5. 非数据描述符

  6. 父类的字典

  7. __getattr__

补充一个顺序的代码:感兴趣的可以按顺序注释掉代码,运行试试

原链接:https://www.cnblogs.com/wickedpriest/p/11984887.html

class Quantity1(object):
    def __get__(self, instance, owner):
        return 2

    def __set__(self, instance, val):
        pass


class Quantity2(object):
    def __get__(self, instance, owner):
        return 5


class A(object):
    val = 6  # 6 父类属性
    x = None


class B(A):
    val = Quantity2()  # 5 非覆盖型描述符
    val = 4  # 4 类属性
    val = Quantity1()  # 2 覆盖型描述符

    def __init__(self):
        super(B, self).__init__()
        self.val = 3

    def __getattr__(self, name):  # 7 __getattr__
        return 7

    def __getattribute__(self, name):  # 1 __getattribute__
        return 1

b = B()
print(b.val)

说了一堆有的没的,其实描述符就是一个特殊的实现,当你的一个对象的属性是描述符时,设置/赋值/读取 这个属性,都会触发这个描述符内部相应实现的方法。从而可以实现一些定制化的内容。

补充

实现类似于 @property 的数据描述符, 下面的代码来自:https://magic.iswbm.com/c04/c04_02.html
如有侵权,请联系删除。

class pro(object):
    """一个数据描述符类,实现了类似于 property 的方法。"""
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        print('init')
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        print('get')
        return self.fget(obj)

    def __set__(self, obj, value):
        print('set')
        return self.fset(obj, value)

    def __delete__(self, obj):
        print('delete')
        self.fdel(obj)

    # 下面的三个方法,返回值都是:数据描述符对象。
    def getter(self, fget):
        print('getter')
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        print('setter')
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        print('deleter')
        return type(self)(self.fget, self.fset, fdel, self.__doc__)


class Student:
    def __init__(self, name):
        self.name = name

    @pro
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    @math.deleter
    def math(self):
        del self._math


if __name__ == "__main__":
    s = Student("anni")  # 1
    print('obj: s')
    s.math = 60  # 2
    print('set to 60')
    del s.math  # 3
    print('deleted')
    s.math = 20  # 4
    print('set to 20')
    print(s.math)  # 5
    print('read')


"""
#1 : 在初始化 Student 类时,因为我们给类方法加了一些装饰器,因此这些装饰器的类实例化时,也会执行。
    1. @pro 会调用 pro(math) 来产生一个 pro 类的实例,即一个名为 math 的数据描述符实例
    2. @math.setter 会调用第一步产生的 math 数据描述符的 .setter(math) 方法 ,它又返回了一个新的名为 math 的数据描述符实例
    3. 至此, math 就不再是 Student 实例的一个方法了,而是一个数据描述符对象
#2 : s.math = 60 来对数据描述符赋值
    1. 它会调用描述符的 __set__(obj, value) 方法, 这里的参数 obj 其实就是 s 对象,即 Student 实例。
    2. __set__(obj, value) 里面调用了 self.fset(obj, value), fset 就是 Student 类中被 @math.setter 装饰的 math 方法(注意,是未经过装饰器装饰的原生的方法)。 因此本质上还是执行原生的 obj.fset(value) 即 s.math(value) (未经过装饰器装饰之前的 Student 类的 math 方法)
#3 : 删除数据描述符,会调用 __delete__ 方法,原理和上面 #2 一样
#4 : 同上
#5 : 同上
"""
posted @ 2021-01-24 21:58  wztshine  阅读(298)  评论(0编辑  收藏  举报