什么是描述符
描述符是Python新式类的关键点之一,它为对象属性提供强大的API,你可以认为描述符是表示对象属性的一个代理。当需要属性时,可根据你遇到的情况,通过描述符进行访问他(摘自Python核心编程)。
实例解析
-
使用类方法创建描述符
就是将某种特殊类型的类的实例指派给另一个类的属性(注意:这里是类属性,而不是对象属性)。而这种特殊类型的类就是实现了
__get__
,__set__
,__delete__
的新式类(即继承object)。
__get__(self, object, type) # 用于得到一个属性的值
__set__(self, obj, val) # 用于为一个属性赋值
__delete__(self, obj) # 删除某个属性时被调用,但很少用到
其中只实现了__set__()
方法的被当做方法描述符,或者是非数据描述符。那些同时实现了__set__()
和__get__()
方法的类被称作数据描述符。
首先定义一个数据描述符类
# 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 Myclass(object):
desc = Descriptor(5)
if __name__ == '__main__':
print Myclass.desc
访问结果为:
访问属性
5
发现访问Myclass的desc属性时,调用了描述符的__get__()
方法。这就达到了描述符的作用(可以改变对象属性的访问)。
调用原理:对于类属性描述符,如果解析器发现属性x是一个描述符的话,在内部通过
type.__getattribute__()
(访问属性时无条件调用,最先调用),它能把Class.x
转换成Class.__dict__[‘x’].__get__(None, Class)
来访问
上面把描述符定义成了类属性,那我们要把他定义成对象属性会有什么样的异同呢?
# 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 Myclass(object):
def __init__(self):
self.desc = Descriptor(5)
if __name__ == '__main__':
myclass = Myclass()
print myclass.desc
输出结果为:
<__main__.Descriptor object at 0x0000000002DFAC18>
并没有像我们预期的那样调用__get__()
方法,只是说他是Descriptor的一个对象。
这是因为当访问实例描述符对象时,
obj.__getattribute__()
会将myclass.desc
转换为type(myclass).__dict__['desc'].__get__(myclass, type(myclass))
,即到类属性中去寻找desc,并调用他的__get__()
方法。而Myclass类中没有desc属性,所以无法访调用到__get__
方法.
描述符是一个类属性,必须定义在类的层次上, 而不能单纯的定义为对象属性。
那么当定义类属性描述符对象和实例属性名字相同时,会有什么样的效果呢?
# 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 Myclass(object):
desc = Descriptor(5)
def __init__(self, desc):
self.desc = desc # 与类属性同名的属性
if __name__ == '__main__':
myclass = Myclass(3)
print myclass.desc
运行结果如下:
设置属性值
访问属性
3
可以看出初始化时访问了描述符的__set__()
方法,访问属性值时访问了描述符的__get__()
方法。这样为什么又调用描述符的方法了呢?
为了解释这个问题,我们要先说一下在python
中访问一个属性的优先级,如下:
- 类属性
- 数据描述符
- 实例属性
- 非数据描述符
- 默认为getattr()(找不到的情况下)
然后我们打印出上面代码类和实例的属性列表:
if __name__ == '__main__':
myclass = Myclass(3)
print "instance: ", myclass.__dict__
print "Class: ", Myclass.__dict__
结果如下:
instance: {}
Class: {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Myclass' objects>, '__weakref__': <attribute '__weakref__' of 'Myclass' objects>, '__doc__': None, '__init__': <function __init__ at 0x00000000030F9438>, 'desc': <__main__.Descriptor object at 0x00000000030E8B70>}
可以发现实例对象的属性中并没有desc
,而相反,类属性中却有它。这是为什么呢?
按照上面的属性访问优先级的理论,数据描述符 > 实例属性。当python发现实例对象的字典中有与定义的描述符有相同名字的对象时,描述符优先,会覆盖掉实例属性。python会改写默认的行为,去调用描述符的方法来代替。
我们来验证一下上面的理论,优先级实例属性 > 非数据描述符
。首先我们定义一下非数据描述符(只有__get__()
方法)
# 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 Myclass(object):
desc = Descriptor(5)
def __init__(self, desc):
self.desc = desc # 与类属性同名的属性
if __name__ == '__main__':
myclass = Myclass(3)
print myclass.desc
print "instance: ", myclass.__dict__
print "Class: ", Myclass.__dict__
运行一下:
3
instance: {'desc': 3}
Class: {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Myclass' objects>, '__weakref__': <attribute '__weakref__' of 'Myclass' objects>, '__doc__': None, '__init__': <function __init__ at 0x0000000002E393C8>, 'desc': <__main__.Descriptor object at 0x0000000002E28B00>}
可以看出,这种情况下访问实例属性,并没有调用描述符的__get__()
方法。而是调用了本身的属性。可以看出理论是正确的。
-
使用属性类型创建描述符
属性是一种有用的特殊类型的描述符。他们是用来处理所有对实例属性的访问,其工作方法和前面说过的描述符类似。通过使用 property(),可以轻松地为任意属性创建可用的描述符。
property
内建函数有四个参数:property(fget=None, fset=None, fdel=None, doc=None)
。
fget:属性获取方法
fset:属性设置方法
fdel:属性删除方法
doc:文档描述
来看一下实现:
class PropertyDesc(object):
def __init__(self):
self._name = ''
def fget(self):
print "Getting: %s" % self._name
return self._name
def fset(self, value):
print "Setting: %s" % value
self._name = 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 = "haha"
print pro.name
del pro.name
结果如下:
Setting: haha
Getting: haha
haha
Deleting: haha
这样实现描述符,虽然简单。但属性多的话就造成代码臃肿不堪。
-
使用属性修饰符创建描述符
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 = "haha"
print pro.name
del pro.name
运行结果如下:
Setting: haha
Getting: haha
haha
Deleting: haha
看,代码运行如初。具体原理就不再赘述。可以打印出PropertyDesc
类和实例pro
的属性列表进行思考。