详解Python描述符的工作原理


Posted in Python onJune 11, 2021

一、前言

其实,在开发过程中,虽然我们没有直接使用到描述符,但是它在底层却无时不刻地被使用到,例如以下这些:

functionbound methodunbound method

装饰器propertystaticmethodclassmethod

是不是都很熟悉?

这些都与描述符有着千丝万缕的关系,这篇文章我们就来看一下描述符背后的工作原理。

二、什么是描述符?

在解释什么是「描述符」之前,我们先来看一个简单的例子。

详解Python描述符的工作原理

这个例子非常简单,我们在类 A 中定义了一个类属性 x,然后打印它的值。

其实,除了直接定类属性之外,我们还可以这样定义一个类属性:

详解Python描述符的工作原理

仔细看,这次类属性 x 不再是一个具体的值,而是一个类 TenTen 中定义了一个 __get__ 方法,返回具体的值。

在 Python 中,允许把一个类属性,托管给一个类,这个属性就是一个「描述符」。

换句话说,「描述符」是一个「绑定行为」的属性。

怎么理解这句话?

回忆一下,我们开发时,一般把「行为」叫做什么?是的,「行为」一般指的是一个方法。

所以我们也可以把「描述符」理解为:对象的属性不再是一个具体的值,而是交给了一个方法去定义。

可以想一下,如果我们用一个方法去定义一个属性,这么做的好处是什么?

有了方法,我们就可以在方法内实现自己的逻辑,最简单的,我们可以根据不同的条件,在方法内给属性赋予不同的值,就像下面这样:

详解Python描述符的工作原理

三、描述符协议

了解了描述符的定义,现在我们把重点放到托管属性的类上。

其实,一个类属性想要托管给一个类,这个类内部实现的方法不能是随便定义的,它必须遵守「描述符协议」,也就是要实现以下几个方法:

__get__(self, obj, type=None)
__set__(self, obj, value)
__delete__(self, obj)

只要是实现了以上几个方法的其中一个,那么这个类属性就可以称作描述符。

另外,描述符又可以分为「数据描述符」和「非数据描述符」:

只定义了 __get___,叫做非数据描述符
除了定义 __get__ 之外,还定义了 __set__ 或 __delete__,叫做数据描述符

它们两者有什么区别,我会在下面详述。

现在我们来看一个包含 __get__ 和 __set__ 方法的描述符例子:

详解Python描述符的工作原理

在这例子中,类属性 age 是一个描述符,它的值取决于 Age 类。

从输出结果来看,当我们获取或修改 age 属性时,调用了 Age 的 __get__ 和 __set__ 方法:

  • 当调用 p1.age 时,__get__ 被调用,参数 obj 是 Person 实例,type 是 type(Person)
  • 当调用 Person.age 时,__get__ 被调用,参数 obj 是 Nonetype 是 type(Person)
  • 当调用 p1.age = 25时,__set__ 被调用,参数 obj 是 Person 实例,value 是25
  • 当调用 p1.age = -1时,__set__ 没有通过校验,抛出 ValueError

其中,调用 __set__ 传入的参数,我们比较容易理解,但是对于 __get__ 方法,通过类或实例调用,传入的参数是不同的,这是为什么?

这就需要我们了解一下描述符的工作原理。

四、描述符的工作原理

要解释描述符的工作原理,首先我们需要先从属性的访问说起。

在开发时,不知道你有没有想过这样一个问题:通常我们写这样的代码 a.b,其背后到底发生了什么?

这里的 a 和 b 可能存在以下情况:

1.a 可能是一个类,也可能是一个实例,我们这里统称为对象

2.b 可能是一个属性,也可能是一个方法,方法其实也可以看做是类的属性

其实,无论是以上哪种情况,在 Python 中,都有一个统一的调用逻辑:

1.先调用 __getattribute__ 尝试获得结果

2.如果没有结果,调用 __getattr__

用代码表示就是下面这样:

详解Python描述符的工作原理

我们这里需要重点关注一下 __getattribute__,因为它是所有属性查找的入口,它内部实现的属性查找顺序是这样的:

1.要查找的属性,在类中是否是一个描述符

2.如果是描述符,再检查它是否是一个数据描述符

3.如果是数据描述符,则调用数据描述符的 __get__

4.如果不是数据描述符,则从 __dict__ 中查找

5.如果 __dict__ 中查找不到,再看它是否是一个非数据描述符

6.如果是非数据描述符,则调用非数据描述符的 __get__

7.如果也不是一个非数据描述符,则从类属性中查找

8.如果类中也没有这个属性,抛出 AttributeError 异常

写成代码就是下面这样:

详解Python描述符的工作原理

如果不好理解,你最好写一个程序测试一下,观察各种情况下的属性的查找顺序。

到这里我们可以看到,在一个对象中查找一个属性,都是先从 __getattribute__ 开始的。

在 __getattribute__ 中,它会检查这个类属性是否是一个描述符,如果是一个描述符,那么就会调用它的 __get__ 方法。但具体的调用细节和传入的参数是下面这样的:

如果 a 是一个实例,调用细节为:

所以我们就能看到上面例子输出的结果。

五、数据描述符和非数据描述符

了解了描述符的工作原理,我们继续来看数据描述符和非数据描述符的区别。

从定义上来看,它们的区别是:

  • 只定义了 __get___,叫做非数据描述符
  • 除了定义 __get__ 之外,还定义了 __set__ 或 __delete__,叫做数据描述符

此外,我们从上面描述符调用的顺序可以看到,在对象中查找属性时,数据描述符要优先于非数据描述符调用。

在之前的例子中,我们定义了 __get__ 和 __set__,所以那些类属性都是数据描述符

我们再来看一个非数据描述符的例子:

详解Python描述符的工作原理

这段代码,我们定义了一个相同名字的属性和方法 foo,如果现在执行 A().foo,你觉得会输出什么结果?

答案是 abc

为什么打印的是实例属性 foo 的值,而不是方法 foo 呢?

这就和非数据描述符有关系了。

我们执行 dir(A.foo),观察结果:

详解Python描述符的工作原理

看到了吗?A 的 foo 方法其实实现了 __get__,我们在上面的分析已经得知:只定义 __get__ 方法的对象,它其实是一个非数据描述符,也就是说,我们在类中定义的方法,其实本身就是一个非数据描述符。

所以,在一个类中,如果存在相同名字的属性和方法,按照上面所讲的 __getattribute__ 中查找属性的顺序,这个属性就会优先从实例中获取,如果实例中不存在,才会从非数据描述符中获取,所以在这里优先查找的是实例属性 foo 的值。

到这里我们可以总结一下关于描述符的相关知识点:

  • 描述符必须是一个类属性
  • __getattribute__ 是查找一个属性(方法)的入口
  • __getattribute__ 定义了一个属性(方法)的查找顺序:数据描述符、实例属性、非数据描述符、类属性
  • 如果我们重写了 __getattribute__ 方法,会阻止描述符的调用
  • 所有方法其实都是一个非数据描述符,因为它定义了 __get__

六、描述符的使用场景

了解了描述符的工作原理,那描述符一般用在哪些业务场景中呢?

在这里我用描述符实现了一个属性校验器,你可以参考这个例子,在类似的场景中去使用它。

首先我们定义一个校验基类 Validator,在 __set__ 方法中先调用 validate 方法校验属性是否符合要求,然后再对属性进行赋值。

详解Python描述符的工作原理

详解Python描述符的工作原理

详解Python描述符的工作原理

现在,当我们对 Person 实例进行初始化时,就可以校验这些属性是否符合预定义的规则了。

七、function与method

我们再来看一下,在开发时经常看到的 functionunbound methodbound method 它们之间到底有什么区别?

来看下面这段代码:

从结果我们可以看出它们的区别:

  • function 准确来说就是一个函数,并且它实现了 __get__ 方法,因此每一个 function 都是一个非数据描述符,而在类中会把 function 放到 __dict__ 中存储
  • 当 function 被实例调用时,它是一个 bound method
  • 当 function 被类调用时, 它是一个 unbound method

function 是一个非数据描述符,我们之前已经讲到了。

而 bound method 和 unbound method 的区别就在于调用方的类型是什么,如果是一个实例,那么这个 function 就是一个 bound method,否则它是一个 unbound method

八、property/staticmethod/classmethod

我们再来看 propertystaticmethodclassmethod

这些装饰器的实现,默认是 C 来实现的。

其实,我们也可以直接利用 Python 描述符的特性来实现这些装饰器,

property 的 Python 版实现:

详解Python描述符的工作原理

详解Python描述符的工作原理

除此之外,你还可以实现其他功能强大的装饰器。

由此可见,通过描述符我们可以实现强大而灵活的属性管理功能,对于一些要求属性控制比较复杂的场景,我们可以选择用描述符来实现。

到此这篇关于详解Python描述符的工作原理的文章就介绍到这了,更多相关Python描述符内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python 线程的暂停, 恢复, 退出详解及实例
Dec 06 Python
Python编程修改MP3文件名称的方法
Apr 19 Python
python根据文章标题内容自动生成摘要的实例
Feb 21 Python
Django使用Channels实现WebSocket的方法
Jul 28 Python
django admin 自定义替换change页面模板的方法
Aug 23 Python
Pytorch实现的手写数字mnist识别功能完整示例
Dec 13 Python
python实现多进程按序号批量修改文件名的方法示例
Dec 30 Python
解决pytorch DataLoader num_workers出现的问题
Jan 14 Python
使用matplotlib动态刷新指定曲线实例
Apr 23 Python
Python多线程threading创建及使用方法解析
Jun 17 Python
Python socket服务常用操作代码实例
Jun 22 Python
pytorch SENet实现案例
Jun 24 Python
用Python爬取各大高校并可视化帮弟弟选大学,弟弟直呼牛X
用python修改excel表某一列内容的操作方法
Windows安装Anaconda3的方法及使用过程详解
Python Flask请求扩展与中间件相关知识总结
Jun 11 #Python
你喜欢篮球吗?Python实现篮球游戏
教你使用TensorFlow2识别验证码
Jun 11 #Python
Python使用OpenCV和K-Means聚类对毕业照进行图像分割
You might like
解决控件遮挡问题:关于有窗口元素和无窗口元素
2007/01/28 PHP
PHP 9 大缓存技术总结
2015/09/17 PHP
php基于openssl的rsa加密解密示例
2016/07/11 PHP
基于yaf框架和uploadify插件,做的一个导入excel文件,查看并保存数据的功能
2017/01/24 PHP
Laravel 读取 config 下的数据方法
2019/10/13 PHP
jquery的clone方法应用于textarea和select的bug修复
2014/06/26 Javascript
深入理解javascript构造函数和原型对象
2014/09/23 Javascript
javascript获取flash版本号的方法
2014/11/20 Javascript
js实现有时间限制消失的图片方法
2015/02/27 Javascript
原生javascript实现匀速运动动画效果
2016/02/26 Javascript
jQuery中队列queue()函数的实例教程
2016/05/03 Javascript
jQuery短信验证倒计时功能实现方法详解
2016/05/25 Javascript
jquery文字填写自动高度的实现方法
2016/11/07 Javascript
vue结合Echarts实现点击高亮效果的示例
2018/03/17 Javascript
在vue里使用codemirror遇到的问题
2018/11/01 Javascript
Vue3.x源码调试的实现方法
2019/10/13 Javascript
vue 弹出遮罩层样式实例
2020/07/22 Javascript
vue 验证两次输入的密码是否一致的方法示例
2020/09/29 Javascript
[55:35]DOTA2-DPC中国联赛 正赛 CDEC vs Dragon BO3 第二场 1月22日
2021/03/11 DOTA
python机器学习理论与实战(六)支持向量机
2018/01/19 Python
pandas实现选取特定索引的行
2018/04/20 Python
使用pycharm生成代码模板的实例
2018/05/23 Python
python寻找list中最大值、最小值并返回其所在位置的方法
2018/06/27 Python
python高效过滤出文件夹下指定文件名结尾的文件实例
2018/10/21 Python
python数据归一化及三种方法详解
2019/08/06 Python
python中shell执行知识点
2020/05/06 Python
python中sys模块是做什么用的
2020/08/16 Python
数控专业毕业生求职信范文
2013/09/21 职场文书
大学生优秀的自我评价分享
2013/10/22 职场文书
创建精神文明单位实施方案
2014/03/08 职场文书
文化建设工作方案
2014/05/12 职场文书
2014年党的群众路线整改措施思想汇报
2014/10/12 职场文书
2014年新农村建设工作总结
2014/12/01 职场文书
初中语文教师研修日志
2015/11/13 职场文书
《夜莺的歌声》教学反思
2016/02/22 职场文书
幼儿园中班教学反思
2016/03/03 职场文书