Descriptor Any object which defines the methods get(), set(), or delete(). When a class attribute is a descriptor, its special binding behavior is triggered upon attribute lookup. Normally, using a.b to get, set or delete an attribute looks up the object named b in the class dictionary for a, but if b is a descriptor, the respective descriptor method gets called.
从表现形式来,一个对象如果实现了__get__, __set__, __del__
方法(三个方法不一定要全都实现),那么这个对象就是一个描述符。__get__, __set__, __del__
的具体声明如下:
# 用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常
__get__(self,instance,owner)
# 在属性分配操作中调用,不会返回任何内容
__set__(self,instance,value)
# 控制删除操作,不会返回内容
__del__(self,instance)
Python是动态类型解释性语言,不像C/C++等静态编译型语言,数据类型在编译时便可以进行验证。在Python中必须添加额外的类型检查代码才能做到这一点,这就是描述符的初衷。初次之外描述符还有诸多其他优点,如保护属性不受修改和自动更新依赖属性的值等。
比如,现在有一个Student类:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
self.name, self.math, self.chinese, self.english
)
stu1 = Student('Alex', 66, 77, 88)
print(stu1)
>>>
<Student: Alex, math:66, chinese: 77, english: 88>
看起来一切都很顺利,但是代码并不像人那么智能,不会根据实际使用场景自动判断数据的合法性,比如在录入会员某门成绩时,不小心多输入了一位或者根本就是不合法的数字,程序是无法感知的,所以我们还需要在代码中加入判断逻辑,完善后代码如下:
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
@property
def math(self):
return self._math
@math.setter
def math(self, score):
if 0 <= score <= 100:
self._math = score
else:
raise ValueError("Valid score must be in [0, 100]")
@property
def chinese(self):
return self._chinese
@chinese.setter
def chinese(self, score):
if 0 <= score <= 100:
self._chinese = score
else:
raise ValueError("Valid score must be in [0, 100]")
@property
def english(self):
return self._english
@english.setter
def english(self, score):
if 0 <= score <= 100:
self._english = score
else:
raise ValueError("Valid score must be in [0, 100]")
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
self.name, self.math, self.chinese, self.english
)
stu1 = Student('Alex', 666, 77, 88)
print(stu1)
>>>
ValueError: Valid score must be in [0, 100]
在上面的代码中,使用了 property 特性,把函数调用伪装成对属性的调用,并对对属性的合法性进行了有效控制,从功能上来说没有问题,但是重复代码率非常高,如果有更多个属性需要做判断,那么代码就会变得更加冗长,虽然property可以让类从外部看起来整洁漂亮,但是却做不到内部同样整洁漂亮。这个时候,描述符就要上场了,它是property的升级版,允许我们为重复的property逻辑编写单独的类来进行处理。
不要尝试在
__int__
方法中使用if ..else
的方法来对属性进行控制,这对于已经存在的实例对象毫无帮助。
使用描述符,对上面的代码进行重写:
class Score:
def __init__(self, default=0):
self._score = default
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Score must be integer')
if not 0 <= value <= 100:
raise ValueError('Valid score must be in [0, 100]')
self._score = value
def __get__(self, instance, owner):
return self._score
def __del__(self):
del self._score
class Student:
math = Score()
chinese = Score()
english = Score()
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
self.name, self.math, self.chinese, self.english
)
stu1 = Student('Alex', 66, 77, 88)
print(stu1)
如上所述,Score 类就是一个描述符对象,当从 Student 的实例访问 math、chinese、english这三个属性的时候,都会经过 Score 类里的特殊的方法,由描述符对象为我们做合法性检查,这就避免了使用property 时出现的大量代码无法复用的问题。
在使用描述符时,为了让描述符能够正常工作,它们必须定义在类的层次上。如果不这么做,那么Python将无法自动调用__get__
和__set__
方法。如下:
class Test():
t1 = Score(20)
def __init__(self):
self.t2 = Score(20)
运行结果>>>
20 <__main__.Score object at 0x10d0987f0>
可以看到,访问类层次上的描述符t1可以自动调用__get__
,但是访问实例层次上的描述符 t2 只会返回描述符本身!
确保实例的数据只属于实例本身
看下面的例子:
t1 = Score(20)
def __init__(self):
self.t2 = Score(20)
class Rethink():
math = Score(80)
me = Rethink()
Alex = Rethink()
print(me.math, Alex.math)
me.math=90
print(me.math, Alex.math)
>>>
80 80
90 90
运行结果出人意料,这说明所有的Test对象的实例竟然都共享相同的math属性!这简直让人无法接受,它也是描述符中最令人感到别扭的地方。为了解决这个问题,我们需要在描述符对象中使用数据字典,__get__
和__set__
的第一个参数告诉我们需要关心哪一个实例,描述符使用这个参数作为字典的key,为每一个实例单独保存一份数据。修改后的Score类的代码如下:
from weakref import WeakKeyDictionary
class Score:
def __init__(self, default=0):
self._score = default
self.data = WeakKeyDictionary()
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Score must be integer')
if not 0 <= value <= 100:
raise ValueError('Valid score must be in [0, 100]')
self.data[instance] = value
def __get__(self, instance, owner):
return self.data.get(instance, self._score)
class Rethink():
math = Score(80)
me = Rethink()
Alex = Rethink()
print(me.math, Alex.math)
me.math=90
print(me.math, Alex.math)
>>>
80 80
90 80
[To be continued...]
参考文档
-
python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解 , 陈洋Cy
-
描述符:其实你不懂我(一),公众号:Python编程时光
网友评论