我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Python 的描述符 descriptor详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Python 的描述符 descriptor详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Python 的描述符 descriptor详解

Python 在 2.2 版本中引入了descriptor(描述符)功能,也正是基于这个功能实现了新式类(new-styel class)的对象模型,同时解决了之前版本中经典类 (classic class) 系统中出现的多重继承中的 MR
2022-06-04

详解Python中的Descriptor描述符类

描述符是调和属性访问的一个类。描述符类可用来获取、设置或删除属性值。描述符对象是在类定义的时候构建在一个类中的。 一般来说,描述符是一个具有绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__()、 __set__
2022-06-04

Python 描述符(Descriptor)入门

很久都没写 Flask 代码相关了,想想也真是惭愧,然并卵,这次还是不写 Flask 相关,不服你来打我啊(就这么贱,有本事咬我啊 这次我来写一下 Python 一个很重要的东西,即 Descriptor (描述符) 初识描述符 老规矩,
2022-06-04

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

在Python中,访问一个属性的优先级顺序按照如下顺序: 1:类属性 2:数据描述符 3:实例属性 4:非数据描述符 5:__getattr__()方法 这个方法的完整定义如下所示:def __getattr(self,attr) :#a
2022-06-04

Python中的Descriptor描述符学习教程

Descriptor是什么?简而言之,Descriptor是用来定制访问类或实例的成员的一种协议。额。。好吧,一句话是说不清楚的。下面先介绍一下Python中成员变量的定义和使用。 我们知道,在Python中定义类成员和C/C++相比得到的
2022-06-04

Python的描述符

1、描述符的定义  描述符是与特定属性互相绑定的一种协议,通过方法被触发修改属性,这些方法包括__get__(),__set__(),__delete__().将这些方法定义在类中,即可实现描述符2、属性与__dict__  Python中
2023-01-30

Python中的descriptor描述器简明使用指南

当定义迭代器的时候,描述是实现迭代协议的对象,即实现__iter__方法的对象。同理,所谓描述器,即实现了描述符协议,即__get__, __set__, 和 __delete__方法的对象。 单看定义,还是比较抽象的。talk is ch
2022-06-04

Python描述符的使用

前言作为一位python的使用者,你可能使用python有一段时间了,但是对于python中的描述符却未必使用过,接下来是对描述符使用的介绍场景介绍为了引入描述符的使用,我们先设计一个非常简单的类:class Product(): d
2023-01-30

深入解析Python中的descriptor描述器的作用及用法

一般来说,一个描述器是一个有“绑定行为”的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__(), __set__(), 和 __delete__() 。有这些方法的对象叫做描述器。
2022-06-04

Python描述符怎么用

这篇文章主要介绍“Python描述符怎么用”,在日常操作中,相信很多人在Python描述符怎么用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Python描述符怎么用”的疑惑有所帮助!接下来,请跟着小编一起来
2023-06-17

python描述符的简单介绍

这篇文章主要介绍“python描述符的简单介绍”,在日常操作中,相信很多人在python描述符的简单介绍问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”python描述符的简单介绍”的疑惑有所帮助!接下来,请跟
2023-06-01

Python中有哪些描述符

这篇文章将为大家详细讲解有关Python中有哪些描述符,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。python可以做什么Python是一种编程语言,内置了许多有效的工具,Python几乎无
2023-06-14

Python中描述符有哪些

这期内容当中小编将会给大家带来有关Python中描述符有哪些,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。python可以做什么Python是一种编程语言,内置了许多有效的工具,Python几乎无所不能,
2023-06-14

Bash中文件描述符的详细介绍

前言 linux将所有内核对象当做文件来处理,系统用一个size_t类型来表示一个文件对象,比如对于文件描述符0就表示系统的标准输入设备STDIN,通常情况下STDIN的值为键盘,如read命令就默
2022-06-04

Python描述符的工作原理是什么

小编给大家分享一下Python描述符的工作原理是什么,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、前言其实,在开发过程中,虽然我们没有直接使用到描述符,但是它在底层却无时不刻地被使用到,例如以下这些:function、
2023-06-15

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

关于@property装饰器 在Python中我们使用@property装饰器来把对函数的调用伪装成对属性的访问。 那么为什么要这样做呢?因为@property让我们将自定义的代码同变量的访问/设定联系在了一起,同时为你的类保持一个简单的访
2022-06-04

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录