Python中的Descriptor描述符
1、什么是描述符?
描述符是Python新式类的关键点之一,它为对象属性提供强大的API,你可以认为描述符是表示对象属性的一个代理。当需要属性时,可根据你遇到的情况,通过描述符进行访问他(摘自Python核心编程)。
2、类属性和实例属性
2.1 属性:__dict__
作用:字典类型,存放本对象的属性,key(键)即为属性名,value(值)即为属性的值,形式为{attr_key : attr_value}。
__dict__
是对象的默认属性,所以每个类对象和实例化对象都有这个属性。
对象属性的访问顺序:
(1)实例对象/类对象的属于描述符的属性
(2)实例属性
(3)类属性
(3)父类属性
(4)__getattr__()
方法
2.2 实例属性的调用顺序
作用:查找类对象或者实例对象的属性(也就是用于获取对象的__dict__属性中的值)
这三个魔法方法的调用顺序如下:
如果 obj = Clz()
, 那么obj.attr
顺序如下:
(1)如果“attr”出现在Clz
的__dict__
中, 且attr
是data descriptor
, 那么调用其__get__
方法
(2)如果“attr”出现在obj
的__dict__
中, 那么直接返回 obj.__dict__['attr']
(3)如果“attr”出现在Clz
的__dict__
中
(3.1)如果attr是non-data descriptor
,那么调用其__get__
方法
(3.2)否则,返回__dict__['attr']
(4)如果Clz
有__getattr__
方法,调用__getattr__
方法
(5)抛出AttributeError
实际上还是上面那个调用顺序。只是结合描述符进行了一些补充关于描述符的补充
3 Descriptor 的定义
描述符本质上是一个类属性,实现描述符的类被称为描述符类。
其中只实现了__set__()
方法的被当做方法描述符,或者是非数据描述符。
那些同时实现了__set__()
和__get__()
方法的类被称作数据描述符。
而魔法方法__get__()
, __set__()
, __delete__()
就用于定义和调用类属性 __dict__
__get__(self, object, type) # 用于得到一个属性的值
__set__(self, obj, val) # 用于为一个属性赋值
__delete__(self, obj) # 删除某个属性时被调用,但很少用到
3.1 描述符的定义和调用初体验
# 描述符类的定义
class MyDescriptor(object):
def __init__(self, value):
self.value = value
# 描述符value的访问
def __get__(self, instance, owner):
return self.value
# 描述符value的定义
def __set__(self, instance, value):
self.value = value
class MyClass(object):
mydescriptor = MyDescriptor(5)
# 在MyClass类中创建一个描述符mydescriptor,重申一下,这是一个类属性。
# 同时可以看到,mydescriptor不仅仅是MyClass类的一个类属性,同时还是MyDescriptor的一个实例对象。
# 这样就将一个类的类属性定义成了另一个类的实例对象。
if __name__ == '__main__':
print (MyClass.mydescriptor) # 输出为 5
发现访问 MyClass 的 mydescriptor 属性时,调用了描述符的__get__()
方法,访问了描述符类的实例属性value.
这就达到了描述符的作用:可以改变了类对象属性的访问。
调用原理:对于类属性描述符,如果解析器发现属性x是一个描述符的话,在内部通过
type.__getattribute__()
(访问属性时无条件调用,最先调用),它能把Class.x
转换成Class.__dict__[‘x’].__get__(None, Class)
来访问
4、描述符与普通属性的区别
上面简单说了几个定义,接下来我们来解决一些实际使用中的细节问题。
4.1 首先我们看一下普通的属性调用
class Test(object):
cls_val = 1
def __init__(self, ins_val):
self.ins_val = ins_val
>>> t=Test(10)
>>> Test.__dict__
{'__module__': '__main__',
'__init__': <function Test.__init__ at 0x03975150>,
'__dict__': <attribute '__dict__' of 'Test' objects>,
'__weakref__': <attribute '__weakref__' of 'Test' objects>,
'__doc__': None,
'cls_val': 1, }
>>> t.__dict__
{'ins_val': 10}
# 没有写在__init__中的类属性在实例化时是不会在实例的__doc__中的。
# 注意,这里并不是说实例t没有属性cls_val。t.cls_val依旧为1。
# ------------------------------------- -------------------------------------
>>> t.cls_val = 20
>>> Test.__dict__
{'__module__': '__main__',
'__init__': <function Test.__init__ at 0x035D5150>,
'__dict__': <attribute '__dict__' of 'Test' objects>,
'__weakref__': <attribute '__weakref__' of 'Test' objects>,
'__doc__': None,
'cls_val': 1, }
>>> t.__dict__
{'ins_val': 10, 'cls_val': 20}
# 更改实例t的属性cls_val,并不影响类Test的类属性cls_val
# ------------------------------------- -------------------------------------
>>> Test.cls_val = 30
>>> Test.__dict__
{'__module__': '__main__',
'__init__': <function Test.__init__ at 0x035D5150>,
'__dict__': <attribute '__dict__' of 'Test' objects>,
'__weakref__': <attribute '__weakref__' of 'Test' objects>,
'__doc__': None,
'cls_val': 30, }
>>> t.__dict__
{'ins_val': 10, 'cls_val': 20}
# 更改了类Test的属性cls_val的值,由于实例在修改类属性之前生成,所以修改类的属性不会影响到实例的属性
以上这段代码证明:
在实例化结束之后,类属性和实例属性互不影响。
4.2 下面我们看看__get__()
方法的调用过程
class Desc(object):
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
print("...__get__...")
print("self : \t\t", self)
print("instance : \t", instance)
print("owner : \t", owner)
return self.value
def __set__(self, instance, value):
print('...__set__...')
print("self : \t\t", self)
print("instance : \t", instance)
print("value : \t", value)
self.value = value
class TestDesc(object):
desc = Desc(666)
# 以下为测试代码
testdesc = TestDesc()
print('testdesc.desc:%s' % testdesc.desc)
print('-' * 40)
print('TestDesc.desc:%s' % TestDesc.desc)
print()
print('=' * 40)
print()
testdesc.desc = 888
print('-' * 40)
print('testdesc.desc:%s' % testdesc.desc)
print('-' * 40)
print('TestDesc.desc:%s' % TestDesc.desc)
# 以下为输出结果
...__get__...
self : <__main__.Desc object at 0x038C2290>
instance : <__main__.TestDesc object at 0x038C22F0>
owner : <class '__main__.TestDesc'>
testdesc.desc:666
----------------------------------------
...__get__...
self : <__main__.Desc object at 0x038C2290>
instance : None
owner : <class '__main__.TestDesc'>
TestDesc.desc:666
========================================
...__set__...
self : <__main__.Desc object at 0x038C2290>
instance : <__main__.TestDesc object at 0x038C22F0>
value : 888
----------------------------------------
...__get__...
self : <__main__.Desc object at 0x038C2290>
instance : <__main__.TestDesc object at 0x038C22F0>
owner : <class '__main__.TestDesc'>
testdesc.desc:888
----------------------------------------
...__get__...
self : <__main__.Desc object at 0x038C2290>
instance : None
owner : <class '__main__.TestDesc'>
TestDesc.desc:888
以上代码说明:
1. 调用实例属性和调用类属性调到的是同一个对象,实际上他们都是由描述符类调用的。
2. 不管是类对象的类属性还是实例对象的实例属性,其实际属性都是描述符的实例对象。
3. 被描述的类属性在被实例化时是被实例对象继承的,示例中testdesc.desc和TestDesc.desc有相同的值,而且修改实例属性testdesc.desc会影响到类属性TestDesc.desc。
4.3 描述符是不能定义成实例属性的
# coding=utf-8
class Descriptor(object):
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
print ("访问属性")
return self.value
def __set__(self, instance, value):
print ("设置属性值")
self.value = value
class TestDesc(object):
classdesc = Descriptor(888)
def __init__(self):
self.insdesc = Descriptor(666)
# 以下为测试代码
testdesc = TestDesc()
print(TestDesc.classdesc)
print(testdesc.classdesc)
print(testdesc.insdesc)
# 以下为输出结果
访问属性
888
访问属性
888
<__main__.Descriptor object at 0x0000025041A64940>
可以看到,实例对象testdesc的 实例属性insdesc 并没有调用__get__()
方法,只是说他是一个Descriptor对象。
这是因为当访问实例描述符对象时,obj.__getattribute__()
会将myclass.desc
转换为type(myclass).__dict__['desc'].__get__(myclass, type(myclass))
,即到类属性中去寻找desc,并调用他的__get__()
方法。而Myclass类中没有desc属性,所以无法访调用到__get__
方法.
描述符是一个类属性,必须定义在类的层次上, 而不能单纯的定义为对象属性。
4. python的property方法
通过使用 property(),可以轻松地为任意属性创建可用的描述符。
property
内建函数有四个参数:property(fget=None, fset=None, fdel=None, doc=None)
。
这四个参数都接受函数类型
class PropertyDesc(object):
def __init__(self):
self.__name = ''
def fget(self):
print ("Getting: %s" % self.__name)
return self.__name
def fset(self, value):
self.__name = value
print ("Setting: %s" % value)
def fdel(self):
print ("Deleting: %s" % self.__name)
del self.__name
name = property(fget, fset, fdel, "I'm the property.")
if __name__ == '__main__':
pro = PropertyDesc()
pro.name = "hellokitty"
print(pro.name)
del pro.name
# 以下为输出结果
Setting: hellokitty
Getting: hellokitty
hellokitty
Deleting: hellokitty
当然也可以使用装饰器的方式实现以上内容:
class PropertyDesc(object):
def __init__(self):
self._name = ''
@property
def name(self):
print ("Getting: %s" % self._name)
return self._name
@name.setter
def name(self, value):
print ("Setting: %s" % value)
self._name = value
@name.deleter
def name(self):
print ("Deleting: %s" %self._name)
del self._name
if __name__ == '__main__':
pro = PropertyDesc()
pro.name = "hello world"
print(pro.name)
del pro.name
# 以下为输出内容
Setting: hello world
Getting: hello world
hello world
Deleting: hello world