Python 的描述符 descriptor详解

发表于 5年以前  | 总阅读数:745 次

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'      

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


    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 "<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!

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


    xiaoming = MathScore(10, 90)

    xiaoming = -10  # 无法判断出错误

对于大多数童鞋,这个问题 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

这确实是种常见的解决方法,但是不得不说这简直丑爆了:

调用成绩再也不能使用 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 "<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!

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

我们知道了 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 类,并在其内实现了getset方法,然后在 MathScore 类中实例化了一个 NonNegative 的实例 score,注意!!!重要的事情说三遍:score 实例是 MathScore 的类属性!!!类属性!!!类属性!!!这个 Mathscore.score 属性同上面 Property 的 score 实例的功能是一样的,只不过 Mathscore.score 调用的 get、set 并不定义在 Mathscore 内,而是定义在 NonNegative 类中,而 NonNegative 类就是一个 descriptor 对象!

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

淡定! 重要的事情这里只说一遍:任何实现 getsetdelete 方法中一至多个的类,就是 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 实例 xiaoming、wangerma 的赋值和调用,并不会产生冲突!因此看起来似乎更类似于 MathScore 的实例属性,但与实例属性不同的是它并不通过 MathScore 实例的读写方法操作值,而总是通过 NonNegative 实例的 getset 对值进行操作,那么它是怎么做到这点的?

注意看 getset 的参数

def get(self, ist, cls): #self:descriptor 实例本身(如 Math.score),ist:调用 score 的实例(如 xiaoming),cls:descriptor 实例所在的类(如MathScore)
...

def __set__(self, ist, value):  #score 就是通过这些传入的 ist 、cls 参数,实现对 MathScore 及其具体实例属性的调用和改写的  
    ...  

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

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

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

O.O! 如果你又秒懂了,那么你可以直接跳到下面写评论了...

看个栗子:假如不仅要判断学生的分数是否为负数,而且还要判学生的学号是否为负值,使用 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 的两个不足的呢?秘诀有三个:

Property 实例本质上是借助类属性,变向对实例属性进行操作,而 NonNegative 实例则是完全通过类属性模拟实例属性,因此实例属性其实根本不存在;

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

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

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

因此,实例对应的属性,压根不在实例自己的作用域内,而是在类作用域的篮子里,只不过我们可以通过 xiaoming.score 这样的方式进行操作而已,所以其实际的调用的逻辑是这样的:下图右侧的实例分别通过红线和黑线对score和std_id 进行操作,他们首先通过类调用相应的类属性,然后类属性通过对应的 descriptor 实例作用域对操作进行处理,并返回给类属性相应结果,最后让实例感知到。

看到这里,很多童鞋可能不淡定了,因为大家都知道在 Python 中采取 xiaoming.score = 10 这样的赋值方式,如果 xiaoming 没有 score 这样的实例属性,必定会自动创建该实例属性,怎么会去调用 MathScore 的 score 呢?

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

其次,Python 为了实现 discriptor 确实对属性的调用顺序做出了相应的调整,这些将会"Python 的 descriptor(下)"中介绍。

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237269次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录