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
- 上述代码中,有两个类,
A
,B
。先看类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
的一个对象。
- 接下来,我们调用一下实例
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'>
"""
我们看一下什么意思:
-
当调用
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
-
当调用
B.a
时,会直接调用B.__dict__['a'].__get__(None,B)
,所以第二处打印的信息中间有个None
-
现在,我们尝试给
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__()
-
如果类属性的描述符对象和实例对象的属性同名,如果查找?
也就是说,如果把类B改成:
class B(object):
a = A()
def __init__(self):
self.val = 20
self.a = 11 # 这里同名的属性
此时调用 b.a
,会如何?
- 当类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
- 当类A是一个非数据描述符,那么实例的字典优先级高,所以会使用实例字典中的数据,即结果:
11
属性查询优先级:
-
obj.__getattribute__()
-
数据描述符
-
实例的字典
-
类的字典
-
非数据描述符
-
父类的字典
-
__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 : 同上
"""