首頁 後端開發 Python教學 Python 的描述符 descriptor详解

Python 的描述符 descriptor详解

Jun 10, 2016 pm 03:06 PM
python

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

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1317
25
PHP教程
1268
29
C# 教程
1246
24
PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

sublime怎麼運行代碼python sublime怎麼運行代碼python Apr 16, 2025 am 08:48 AM

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

Golang vs. Python:性能和可伸縮性 Golang vs. Python:性能和可伸縮性 Apr 19, 2025 am 12:18 AM

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。

vscode在哪寫代碼 vscode在哪寫代碼 Apr 15, 2025 pm 09:54 PM

在 Visual Studio Code(VSCode)中編寫代碼簡單易行,只需安裝 VSCode、創建項目、選擇語言、創建文件、編寫代碼、保存並運行即可。 VSCode 的優點包括跨平台、免費開源、強大功能、擴展豐富,以及輕量快速。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

See all articles