Python 在 2.2 版本中引入了descriptor(描述符)功能,也正是基于这个功能实现了新式类(new-styel class)的对象模型,同时解决了之前版本中经典类 (classic class) 系统中出现的多重继承中的 MRO(Method Resolution Order) 问题,另外还引入了一些新的概念,比如 classmethod, staticmethod, super, Property 等。因此理解 descriptor 有助于更好地了解 Python 的运行机制。
那么什么是 descriptor 呢?
简而言之:descriptor 就是一类实现了__get__(), __set__(), __delete__()方法的对象。
Orz...如果你瞬间顿悟了,那么请收下我的膝盖;
O_o!...如果似懂非懂,那么恭喜你!说明你潜力很大,咱们可以继续挖掘:
引言
对于陌生的事物,一个具体的栗子是最好的学习方式,首先来看这样一个问题:假设我们给一次数学考试创建一个类,用于记录每个学生的学号、数学成绩、以及提供一个用于判断是否通过考试的check 函数:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
self.score = score
def check(self):
if self.score >= 60:
return 'pass'
else:
return 'failed'
</div>
很简单一个示例,看起来运行的不错:
xiaoming = MathScore(10, 90) xiaoming.score Out[3]: 90 xiaoming.std_id Out[4]: 10 xiaoming.check() Out[5]: 'pass'</div>
但是会有一个问题,比如手一抖录入了一个负分数,那么他就得悲剧的挂了:
xiaoming = MathScore(10, -90) xiaoming.score Out[8]: -90 xiaoming.check() Out[9]: 'failed'</div>
这显然是一个严重的问题,怎么能让一个数学 90+ 的孩子挂科呢,于是乎一个简单粗暴的方法就诞生了:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.score = score
def check(self):
if self.score >= 60:
return 'pass'
else:
return 'failed'
</div>
上面再类的初始化函数中增加了负数判断,虽然不够优雅,甚至有点拙劣,但这在实例初始化时确实工作的不错:
xiaoming = MathScore(10, -90)
Traceback (most recent call last):
File "<ipython-input-12-6faad631790d>", line 1, in <module>
xiaoming = MathScore(10, -90)
File "C:/Users/xu_zh/.spyder2-py3/temp.py", line 14, in __init__
raise ValueError("Score can't be negative number!")
ValueError: Score can't be negative number!
</div>
OK, 但我们还无法阻止实例对 score 的赋值操作,毕竟修改成绩也是常有的事:
xiaoming = MathScore(10, 90) xiaoming = -10 # 无法判断出错误</div>
对于大多数童鞋,这个问题 so easy 的啦:将 score 变为私有,从而禁止 xiaoming.score 这样的直接调用,增加一个 get_score 和 set_score 用于读写:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
def get_score(self):
return self.__score
def set_score(self, value):
if value < 0:
raise ValueError("Score can't be negative number!")
self.__score = value
</div>
这确实是种常见的解决方法,但是不得不说这简直丑爆了:
调用成绩再也不能使用 xiaoming.score 这样自然的方式,需要使用 xiaoming.get_score() ,这看起来像口吃在说话!
还有那反人类的下划线和括号...那应该只出现在计算机之间窃窃私语之中...
赋值也无法使用 xiaoming.score = 80, 而需使用 xiaoming.set_score(80), 这对数学老师来说,太 TM 不自然了 !!!
作为一门简洁优雅的编程语言,Python 是不会坐视不管的,于是其给出了 Property 类:
Property 类
先不管 Property 是啥,咱先看看它是如何简洁优雅的解决上面这个问题的:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
def __get_score__(self):
return self.__score
def __set_score__(self, value):
if value < 0:
raise ValueError("Score can't be negative number!")
self.__score = value
score = property(__get_score__, __set_score__)
</div>
与上段代码相比,主要是在最后一句实例化了一个 property 实例,并取名为 score, 这个时候,我们就能如此自然的对 instance.__score 进行读写了:
xiaoming = MathScore(10, 90)
xiaoming.score
Out[30]: 90
xiaoming.score = 80
xiaoming.score
Out[32]: 80
xiaoming.score = -90
Traceback (most recent call last):
File "<ipython-input-33-aed7397ed552>", line 1, in <module>
xiaoming.score = -90
File "C:/Users/xu_zh/.spyder2-py3/temp.py", line 28, in __set_score__
raise ValueError("Score can't be negative number!")
ValueError: Score can't be negative number!
</div>
WOW~~一切工作正常!
嗯,那么问题来了:它是怎么工作的呢?
先看下 property 的参数:
class property(fget=None, fset=None, fdel=None, doc=None) #拷贝自 Python 官方文档
它的工作方式:
实例化 property 实例(我知道这是句废话);
调用 property 实例(比如xiaoming.score)会直接调用 fget,并由 fget 返回相应值;
对 property 实例进行赋值操作(xiaoming.score = 80)则会调用 fset,并由 fset 定义完成相应操作;
删除 property 实例(del xiaoming),则会调用 fdel 实现该实例的删除;
doc 则是该 property 实例的字符说明;
fget/fset/fdel/doc 需自定义,如果只设置了fget,则该实例为只读对象;
这看起来和本篇开头所说的 descriptor 的功能非常相似,让我们回顾一下 descriptor:
“descriptor 就是一类实现了__get__(), __set__(), __delete__()方法的对象。”
@~@ 如果你这次又秒懂了,那么请再次收下我的膝盖 Orz...
另外,Property 还有个装饰器语法糖 @property,其所实现的功能与 property() 完全一样:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
@property
def score(self):
return self.__score
@score.setter
def score(self, value): #注意方法名称要与上面一致,否则会失效
if value < 0:
raise ValueError("Score can't be negative number!")
self.__score = value
</div>
我们知道了 property 实例的工作方式了,那么问题又来了:它是怎么实现的?
事实上 Property 确实是基于 descriptor 而实现的,下面进入我们的正题 descriptor 吧!
descriptor 描述符
照样先不管 descriptor 是啥,咱们还是先看栗子,对于上面 Property 实现的功能,我们可以通过自定义的 descriptor 来实现:
class NonNegative():
def __init__(self):
pass
def __get__(self, ist, cls):
return 'descriptor get: ' + str(ist.__score ) #这里加上字符描述便于看清调用
def __set__(self, ist, value):
if value < 0:
raise ValueError("Score can't be negative number!")
print('descriptor set:', value)
ist.__score = value
class MathScore():
score = NonNegative()
def __init__(self, std_id, score):
self.std_id = std_id

