Python 的描述符 descriptor详解

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 类,并在其内实现了__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 实例 xiaoming、wangerma 的赋值和调用,并不会产生冲突!因此看起来似乎更类似于 MathScore 的实例属性,但与实例属性不同的是它并不通过 MathScore 实例的读写方法操作值,而总是通过 NonNegative 实例的 __get__ 和 __set__ 对值进行操作,那么它是怎么做到这点的?

注意看 __get__、__set__ 的参数

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(下)”中介绍。

(0)

相关推荐

  • python的描述符(descriptor)、装饰器(property)造成的一个无限递归问题分享

    分享一下刚遇到的一个小问题,我有一段类似于这样的python代码: 复制代码 代码如下: # coding: utf-8 class A(object): @property     def _value(self): #        raise AttributeError("test")         return {"v": "This is a test."} def __getattr__(self, key):         p

  • Python黑魔法Descriptor描述符的实例解析

    在Python中,访问一个属性的优先级顺序按照如下顺序: 1:类属性 2:数据描述符 3:实例属性 4:非数据描述符 5:__getattr__()方法  这个方法的完整定义如下所示: def __getattr(self,attr) :#attr是self的一个属性名 pass; 先来阐述下什么叫数据描述符. 数据描述符是指实现了__get__,__set__,__del__方法的类属性(由于Python中,一切皆是对象,所以你不妨把所有的属性也看成是对象) PS:个人觉得这里最好把数据描述符

  • Python中属性和描述符的正确使用

    关于@property装饰器 在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问. 那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联系在了一起,同时为你的类保持一个简单的访问属性的接口. 举个栗子,假如我们有一个需要表示电影的类: class Movie(object): def __init__(self, title, description, score, ticket): self.title = title self.

  • 解密Python中的描述符(descriptor)

    Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解.这些特性包括列表/集合/字典推导式,属性(property).以及装饰器(decorator).对于大部分特性来说,这些"中级"的语言特性有着完善的文档,并且易于学习. 但是这里有个例外,那就是描述符.至少对于我来说,描述符是Python语言核心中困扰我时间最长的一个特性.这里有几点原因如下: 1.有关描述符的官方文档相当难懂,而且没有包含优秀的示例告诉你为什么需要编写描述符(我得为Raymond Hettinger辩

  • Python中的类与对象之描述符详解

    描述符(Descriptors)是Python语言中一个深奥但却重要的一部分.它们广泛应用于Python语言的内核,熟练掌握描述符将会为Python程序员的工具箱添加一个额外的技巧.为了给接下来对描述符的讨论做一些铺垫,我将描述一些程序员可能会在日常编程活动中遇到的场景,然后我将解释描述符是什么,以及它们如何为这些场景提供优雅的解决方案.在这篇总结中,我会使用新样式类来指代Python版本. 1.假设一个程序中,我们需要对一个对象属性执行严格的类型检查.然而,Python是一种动态语言,所以并不

  • Python 描述符(Descriptor)入门

    很久都没写 Flask 代码相关了,想想也真是惭愧,然并卵,这次还是不写 Flask 相关,不服你来打我啊(就这么贱,有本事咬我啊 这次我来写一下 Python 一个很重要的东西,即 Descriptor (描述符) 初识描述符 老规矩, Talk is cheap,Show me the code. 我们先来看看一段代码 classPerson(object): """""" #---------------------------------

  • Python 的描述符 descriptor详解

    Python 在 2.2 版本中引入了descriptor(描述符)功能,也正是基于这个功能实现了新式类(new-styel class)的对象模型,同时解决了之前版本中经典类 (classic class) 系统中出现的多重继承中的 MRO(Method Resolution Order) 问题,另外还引入了一些新的概念,比如 classmethod, staticmethod, super, Property 等.因此理解 descriptor 有助于更好地了解 Python 的运行机制.

  • Bottle框架中的装饰器类和描述符应用详解

    最近在阅读Python微型Web框架Bottle的源码,发现了Bottle中有一个既是装饰器类又是描述符的有趣实现.刚好这两个点是Python比较的难理解,又混合在一起,让代码有些晦涩难懂.但理解代码之后不由得为Python语言的简洁优美赞叹.所以把相关知识和想法稍微整理,以供分享. 正文 Bottle是Python的一个微型Web框架,所有代码都在一个bottle.py文件中,只依赖标准库实现,兼容Python 2和Python 3,而且最新的稳定版0.12代码也只有3700行左右.虽然小,但

  • Python描述器descriptor详解

    前面说了descriptor,这个东西其实和Java的setter,getter有点像.但这个descriptor和上文中我们开始提到的函数方法这些东西有什么关系呢? 所有的函数都可以是descriptor,因为它有__get__方法. 复制代码 代码如下: >>> def hello():      pass  >>> dir(hello)  ['__call__', '__class__', '__delattr__', '__dict__', '__doc__',

  • python Scala函数与访问修辞符实例详解

    目录 常规函数 可变参数函数 使用名字调用函数 匿名函数 访问修饰符 常规函数 object Demo { def main(args: Array[String]) { println( "Returned Value : " + addInt(5,7) ); // 普通调用 println( "Returned Value : " + addInt(a=5,b=7) ); // 指定参数调用 } // 方法 默认参数 b = 7 def addInt( a:In

  • Python Pandas数据处理高频操作详解

    目录 引入依赖 算法相关依赖 获取数据 生成df 重命名列 增加列 缺失值处理 独热编码 替换值 删除列 数据筛选 差值计算 数据修改 时间格式转换 设置索引列 折线图 散点图 柱状图 热力图 66个最常用的pandas数据分析函数 从各种不同的来源和格式导入数据 导出数据 创建测试对象 查看.检查数据 数据选取 数据清理 筛选,排序和分组依据 数据合并 数据统计 16个函数,用于数据清洗 1.cat函数 2.contains 3.startswith/endswith 4.count 5.ge

  • python模块之re正则表达式详解

    一.简单介绍 正则表达式是一种小型的.高度专业化的编程语言,并不是python中特有的,是许多编程语言中基础而又重要的一部分.在python中,主要通过re模块来实现. 正则表达式模式被编译成一系列的字节码,然后由用c编写的匹配引擎执行.那么正则表达式通常有哪些使用场景呢? 比如为想要匹配的相应字符串集指定规则: 该字符串集可以是包含e-mail地址.Internet地址.电话号码,或是根据需求自定义的一些字符串集: 当然也可以去判断一个字符串集是否符合我们定义的匹配规则: 找到字符串中匹配该规

  • Python命令行解析模块详解

    本文研究的主要是Python命令行解析模块的相关内容,具体如下. Python命令行常见的解析器有两种,一是getopt模块,二是argparse模块.下面就解读下这两种解析器. getopt模块 这个模块可以帮助脚本解析命令行参数,一般是sys.argv[1:].它遵循着Unix的getopt()函数相同的约定(用-/--指定命令参数).这个模块提供两个函数(getopt.getopt()/getopt.gnu_getopt())和一个参数异常(getopt.GetoptError). 这里重

  • Python 统计字数的思路详解

     问题描述: 用 Python 实现函数 count_words(),该函数输入字符串 s 和数字 n,返回 s 中 n 个出现频率最高的单词.返回值是一个元组列表,包含出现次数最高的 n 个单词及其次数,即 [(<单词1>, <次数1>), (<单词2>, <次数2>), ... ],按出现次数降序排列. 您可以假设所有输入都是小写形式,并且不含标点符号或其他字符(只包含字母和单个空格).如果出现次数相同,则按字母顺序排列. 例如: print count

  • python正则-re的用法详解

    天在刷题的时候用到了正则,用的过程中就感觉有点不太熟练了,很久没有用正则都有点忘了.所以现在呢,我们就一起来review一下python中正则模块re的用法吧. 今天是review,所以一些基础的概念就不做介绍了,先来看正则中的修饰符以及它的功能: 修饰符 •re.I 使匹配对大小写不敏感 •re.L 做本地化识别匹配 •re.M 多行匹配,影响^和$ •re.S 使.匹配包括换行在内的所有字符 •re.U 根据Unicode字符集解析字符.这个标志影响\w \W \b \B •re.X 该标志

随机推荐