ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Python中基于descriptor的一些概念(2)——descriptor对象

2021-12-05 20:02:53  阅读:208  来源: 互联网

标签:__ 基于 实例 Python self MathScore descriptor score


文章目录

上文讲了Python从2.2开始基于新引入的descriptor实现了新式类,并讲解了新式类的一些概念和方法属性,那么什么是descriptor呢?

引言

简而言之:descriptor是一个绑定了特定访问方法的类属性,这些访问方法重写了descriptor protocol中的三个方法,分别是__get__, __set__,__del__方法。如果三个中任一一个方法在对象中定义了,就说这个对象是一个descriptor对象,可以把这个对象赋值给其它属性。descriptor protocol可以看成是一个有三个方法的接口。

对于陌生的事物,一个具体的栗子是最好的学习方式,首先来看这样一个问题:假设我们给一次数学考试创建一个类,用于记录每个学生的学号、数学成绩、以及提供一个用于判断是否通过考试的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'

很简单一个示例,看起来运行的不错:

xiaoming = MathScore(10, 90)
 
xiaoming.score
Out[3]: 90
 
xiaoming.std_id
Out[4]: 10
 
xiaoming.check()
Out[5]: 'pass'

但是会有一个问题,比如手一抖录入了一个负分数,那么他就得悲剧的挂了:

xiaoming = MathScore(10, -90)
 
xiaoming.score
Out[8]: -90
 
xiaoming.check()
Out[9]: 'failed'

这显然是一个严重的问题,怎么能让一个数学 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'

xiaoming = MathScore(10, -90)

上面在类的初始化函数中增加了负数判断,虽然不够优雅,甚至有点拙劣,但这在实例初始化时确实工作的不错:

Traceback (most recent call last):
 
 File "demo.py", line 15, in <module>
  xiaoming = MathScore(10, -90)
 
 File "demo.py", line 14, in __init__
  raise ValueError("Score can't be negative number!")
 
ValueError: Score can't be negative number!

OK, 但我们还无法阻止实例对 score 的赋值操作,毕竟修改成绩也是常有的事:

xiaoming = MathScore(10, 90)
 
xiaoming = -10  # 无法判断出错误

对于大多数童鞋,这个问题 so easy 的啦:将 score 变为私有,从而禁止 xiaoming.score 这样的直接调用,增加一个get_scoreset_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

这确实是种常见的解决方法,但是不得不说这简直丑爆了:调用成绩再也不能使用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__)

与上段代码相比,主要是在最后一句实例化了一个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 "demo.py", line 1, in <module>
  xiaoming.score = -90
 
 File "demo.py", line 28, in __set_score__
  raise ValueError("Score can't be negative number!")
 
ValueError: Score can't be negative number!

WOW~~一切工作正常!
嗯,那么问题来了:它是怎么工作的呢?

先看下Python官方文档property,它的工作方式:

实例化 property 实例(我知道这是句废话);

调用 property 实例(比如xiaoming.score)会直接调用fget,并由fget返回相应值;

property实例进行赋值操作(xiaoming.score = 80)则会调用fset,并由fset定义完成相应操作;

删除property实例(del xiaoming),则会调用fdel实现该实例的删除;

doc则是该property实例的字符说明;

fget/fset/fdel/doc需自定义,如果只设置了fget,则该实例为只读对象;
这看起来和本篇开头所说的descriptor的功能非常相似。另外,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

我们知道了 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
    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'

我们新定义了一个NonNegative类,并在其内实现了__get____set__方法,然后在MathScore类中实例化了一个NonNegative的实例score,注意:score实例是MathScore 的类属性! 这个Mathscore.score属性同上面property的score实例的功能是一样的,只不过Mathscore.score调用的get、set并不定义在Mathscore内,而是定义在NonNegative类中,而NonNegative类就是一个descriptor对象!

纳尼?NonNegative类的定义中可没见到半个 “descriptor” 的字样,怎么就成了descriptor对象???

淡定! 任何实现__get____set____delete__方法中一至多个的类,就是descriptor对象。所以NonNegative自然是一个descriptor对象。

那么descriptor对象与普通类有什么特别之处呢?先不急,来看看上段代码的效果:

xiaoming = MathScore(10, 90)
 
xiaoming.score
Out[67]: 'descriptor get: 90'
 
xiaoming.score = 80
descriptor set: 80
 
wangerma = MathScore(11, 70)
 
wangerma.score
Out[70]: 'descriptor get: 70'
 
wangerma.score = 60
Out[70]: descriptor set: 60
 
wangerma.score
Out[73]: 'descriptor get: 60'
 
xiaoming.score
Out[74]: 'descriptor get: 80'
 
xiaoming.score = -90
ValueError: Score can't be negative number!

可以发现,MathScore.score虽然是一个类属性,但它却可以通过实例进行赋值,且面对不同的MathScore实例xiaomingwangerma的赋值和调用,并不会产生冲突!因此看起来似乎更类似于MathScore的实例属性,但与实例属性不同的是它并不通过MathScore实例的读写方法操作值,而总是通过NonNegative实例的__get____set__对值进行操作。

那么它是怎么做到的呢?

注意看__get____set__的参数:

def __get__(self,ist,cls):
    """
    self:descriptor实例本身(如 Math.score)
    ist:调用score的实例(如 xiaoming)
    cls:descriptor实例所在的类(如MathScore)
    """
    pass
def __set__(self,ist,value):
    # score就是通过这些传入的ist、cls参数,实现对MathScore及其具体实例属性的调用和改写的
    pass

OK, 现在我们基本搞清了descriptor实例是如何实现对宿主类的实例属性进行模拟的。事实上property实例的实现方式与上面的NonNegative实例类似。那么我们既然有了propery,为什么还要去自定义descriptor呢?

答案在于:更加逼真的模拟实例属性(想想MathScore.__init__里面那恶心的判断语句),还有最重要的是:代码重用!!!

简而言之:通过单个descriptor对象,可以更加逼真的模拟实例属性,并且可以实现对宿主类实例的多个实例属性进行操作。

看个栗子:假如不仅要判断学生的分数是否为负数,而且还要判学生的学号是否为负值,使用 property 的实现方式是这样子的:

class MathScore():
  def __init__(self, std_id, score):
    if std_id < 0:
      raise ValueError("Can't be negative number!")
    self.__std_id = std_id
    if score < 0:
      raise ValueError("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("Can't be negative number!")
    self.__score = value
  @property
  def std_id(self):
    return self.__std_id
  @std_id.setter
  def std_id(self, idnum):
    if idnum < 0:
      raise ValueError("Can't be negative nmuber!")
    self.__std_id = idnum

property实例最大的问题是:

无法影响宿主类实例的初始化,所以咱必须在__init__加上那丑恶的 if
单个property实例仅能针对宿主类实例的单个属性,如果需要对多个属性进行控制,则必须定义多个property实例, 这真是太蛋疼了!
但是自定义descriptor可以很好的解决这个问题,看下实现:

class NonNegative():
  def __init__(self):
    self.dic = dict()
  def __get__(self, ist, cls):
    print('Description get', ist)
    return self.dic[ist]
  def __set__(self, ist, value):
    print('Description set', ist, value)
    if value < 0:
      raise ValueError("Can't be negative number!")
    self.dic[ist] = value

class MathScore():
  score = NonNegative()  
  std_id = NonNegative()  
  def __init__(self, std_id, score):
    #这里并未创建实例属性 std_id 和 score, 而是调用 MathScore.std_id 和 MathScore.score  
    self.std_id = std_id
    self.score = score 
  def check(self):
    if self.score >= 60:
      return 'pass'
    else:
      return 'failed'

哈哈~! MathScore.__init__内终于没了if ,代码也比上面的简洁不少,但是功能一个不少,且实例之间不会相互影响:

事实上,MathScore多个实例的同一个属性,都是通过单个MathScore类的相应类属性(也即NonNegative实例)操作的,这同property一致,但它又是怎么克服property的两个不足的呢?秘诀有三个:

  1. property实例本质上是借助类属性,变向对实例属性进行操作,而NonNegative实例则是完全通过类属性模拟实例属性,因此实例属性其实根本不存在;
  2. NonNegative实例使用字典记录每个MathScore实例及其对应的属性值,其中keyMathScore实例名。
    比如score实例就是使用dic = {'Zhangsan':50, 'Lisi':90}记录每个实例对应的score值,从而确保可以实现对MathScore实例属性的模拟;
  3. MathScore通过在__init__内直接调用类属性,从而实现对实例属性初始化赋值的模拟,而property则不能,因为property实例(也即MathScore的类属性)是真实的操作MathScore实例传入的实例属性以达到目的,但如果在初始化程序中传入的不是实例属性,而是类属性(也即property实例本身),则会陷入无限递归(PS:想一下如果将前一个property实例实现中的self.__score改成这里的self.score会发生什么)。

这三点看的似懂非懂,没关系,来个比喻:

每个descriptor实例(MathScore.scoreMathScore.std_id)都是类作用域里的一个篮子,篮子里放着写着每个MathScore实例名字的盒子('zhangsan','lisi'),同一个篮子里的盒子只记录同样属性的值(比如score篮子里的盒子只记录分数值),当MathScore的实例对相应属性进行操作时,则找到对应的篮子,取出标有该实例名字的盒子,并对其进行操作。

因此,实例对应的属性,压根不在实例自己的作用域内,而是在类作用域的篮子里,只不过我们可以通过xiaoming.score这样的方式进行操作而已,所以其实际的调用的逻辑是这样的:下图右侧的实例分别通过红线和黑线对score和std_id 进行操作,他们首先通过类调用相应的类属性,然后类属性通过对应的 descriptor 实例作用域对操作进行处理,并返回给类属性相应结果,最后让实例感知到。
在这里插入图片描述
看到这里,很多童鞋可能不淡定了,因为大家都知道在 Python 中采取 xiaoming.score = 10 这样的赋值方式,如果xiaoming没有score这样的实例属性,必定会自动创建该实例属性,怎么会去调用MathScore的score呢?

首先,要鼓掌!!! 给想到这点的童鞋点赞!!!其实上面在说 property的时候这个问题就产生了。

其次,Python 为了实现descriptor确实对属性的调用顺序做出了相应的调整,这就是上一篇里讲到的新式类的MRO问题。

参考

Python的descriptor(上)

标签:__,基于,实例,Python,self,MathScore,descriptor,score
来源: https://blog.csdn.net/wq_0708/article/details/121734135

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有