python中@property以及描述符descriptor详解
python一直以代码简洁优雅而著称,这篇文章介绍的小技巧,就是如何优雅地对一个类的属性进行赋值和取值。不过不仅仅如此,本文章还为类属性的查找顺序,以及装饰器在类方法的使用打下了基础。
文章目录
- 待解决的问题
- 属性变为方法
- @property装饰器
- 描述符
- 描述符的改进
- 扩展知识点
- 描述符添加额外方法
- 属性查找顺序
- 函数也是描述符
待解决的问题
先看下面的例子
class People:def __init__(self):self.age = 3if __name__ == '__main__':xiaofu = People()print('-' * 10)print(xiaofu.age) # 3xiaofu.age=100print(xiaofu.age) # 100xiaofu.age=-10print(xiaofu.age) # -10
对于定义的类People
有一个实例属性age
表示实例的年龄,对于实例xiaofu
,我们可以任意设置其年龄,即使有些数字根本就不合理。
这就是我们这篇文章要解决的问题,如何在不改变操作习惯的前提下限制对属性的合理赋值范围,例如年龄age
不能为负值。
属性变为方法
首先是最原始的办法,不直接暴露属性,而是暴露出两个方法分别用于赋值和取值
class People:def __init__(self):self.__age = 3def set_age(self, value):if value >= 0:self.__age = valueelse:print('Age can not be negative value')def get_age(self):return self.__ageif __name__ == '__main__':xiaofu = People()print(xiaofu.get_age()) # 3xiaofu.set_age(-10) # Age can not be negative valueprint(xiaofu.get_age()) # 3xiaofu.set_age(10)print(xiaofu.get_age()) # 10
这是个看似有效其实很笨的方法。
首先,用户为了操作一个属性,必须记住两个方法,要是方法命名规则规范还好,不规范那简直就是要了亲命。再者如果有多个类似的属性,一下子类就变得重复臃肿了起来,不符合代码DRY的原则;
DRY: Don’t Repeat Yourself
其次,大家也都知道,python根本就没有绝对的内置属性,不让用户直接访问也只是通过改了个名字曲线救国而已,dir(xiaofu)
或者xiaofu.__dict__
一查就能查到,像刚才的__age
就改成了_People__age
。
dir()
用于显示所有的属性,方法也是一种属性所以也会显示;__dict__
只是显示用户自定义的属性,并不会显示方法
所以这种方式在别的语言也许可以,在python中是坚决不行的。
@property装饰器
来看看第一种改进措施
class People:def __init__(self):self.__age = 3@propertydef age(self):return self.__age@age.setterdef age(self, value):if value >= 0:self.__age = valueelse:print('Age can not be negative value')if __name__ == '__main__':xiaofu = People()print('-' * 10)print(xiaofu.age) # 3xiaofu.age=100print(xiaofu.age) # 100xiaofu.age=-10 # Age can not be negative valueprint(xiaofu.age) # 100
这里通过@property
装饰器将其中一个方法变成了属性,方法的名字变成了属性可以被直接访问。还对一个同样名字的方法用装饰器变成了setter
,当向该属性赋值的时候会调用该方法。
通过打印的结果看,恢复了原先的属性的调用方式,而不再是两个难记的方法。虽说用户还是可以通过dir()
或者__dict__
查看到真正的属性,但是只要不是有人故意找事,这种方式已经基本帮我们完成了比较优雅地属性取值范围限制。
但是@property
还是不能复用,不满足DRY的原则。通过对两个方法添加装饰器来达到对外地统一接口,使用起来很舒服,但假设有10个这种属性,就得创造20个这种方法,这么臃肿的一个类想想就很头疼。
必须得把这两种方法单独提出来,去耦合。
描述符
再来看看第二种改进措施。
python中规定,只要一个对象包含__get__
方法,就是描述符对象(Descriptor)。当然,通常的描述符对象还包含__set__
和__delete__
方法。只包含__get__
方法的描述符叫做非数据描述符(Non-Data descriptor),只读,而同时包含了另外两个方法的叫做数据描述符(Data descriptor),可读可写。
如果类的属性是描述符对象,在执行取值操作时,会执行描述符对象的__get__
方法,在执行赋值操作时,会执行描述符对象的__set__
方法,而删除操作时,会执行__delete__
方法。
关于删除属性,如果该属性不是描述符,会抛出异常,该属性只读。如果该属性是描述符但是没有实现
__delete__
方法,也会抛出只读的异常。如果类中定义了__delattr__
,其优先级高于属性的__delete__
class MyDescriptor:def __init__(self,default):self.val=defaultdef __get__(self, instance, owner):passdef __set__(self, instance, value):passdef __delete__(self, instance):passclass People:age = MyDescriptor(0)
这是一个典型的描述符类的结构以及调用方法,这里方法的参数有点多,我们一个个来看。
- instance - 这是包含了描述符对象的类实例,这里就是具体的People对象
- self - 和所有其他类一样,self表示当前类的实例,这里就是具体的MyDescriptor对象
- owner - 是instance这个实例对应的类名,这里就是People类
- value - 赋值时候等号右边传递进来的值
下面是具体实现的代码
class MyDescriptor:def __init__(self,default):self.val=defaultdef __get__(self, instance, owner):return self.valdef __set__(self, instance, value):if value >=0:self.val = valueelse:print('Negative value is not allowed')def __delete__(self, instance):print('delete')class People:age = MyDescriptor(0)if __name__ == '__main__':xiaofu = People()print('-' * 10)print(xiaofu.age) # 0xiaofu.age=100del xiaofu.age # deleteprint(xiaofu.age) # 100xiaofu.age=-10 # Negative value is not allowedprint(xiaofu.age) # 100
这样就真正达到了优雅地限制属性取值范围的目的。
需要注意,必须在class级别定义描述符对象,如果像下面这样并不会自动调用描述符的__get__
或者__set__
方法
class People:def __init__(self):self.age = MyDescriptor(0)
我承认这是个很让人困惑的设定,同时这也引出了描述符的一个大问题,那就是多个实例共享同一描述符对象
if __name__ == '__main__':xiaofu = People()zhangsan = People()print('-' * 10)print(xiaofu.age) # 0print(zhangsan.age) # 0xiaofu.age=100print(xiaofu.age) # 100print(zhangsan.age) # 100
因为实例都会有类属性,所以xiaofu和zhangsan都会有age这个属性。这里即使只操作xiaofu的年龄,zhangsan的年龄也会跟着变,这显然不合逻辑,所以还需要对现有的描述符进行改进。
描述符的改进
既然在赋值和取值的时候都会传递进来instance
变量,那么就可以在描述符类中添加一个字典,按照不同instance
做为key去存储值应该就可以了。这里可以用原生字典,不过为了减少内存泄漏,我采用了弱引用的字典
python中是按照内存被变量的引用个数来决定是否回收,引用变为0则回收。弱引用就是不占用内存引用个数的变量,当其余的引用都消失后,内存自动被回收。通常用于缓存的数据。
import weakrefclass MyDescriptor:def __init__(self, default):self.val = defaultself.info = weakref.WeakKeyDictionary()def __get__(self, instance, owner):return self.info.get(instance, self.val)def __set__(self, instance, value):if value >= 0:self.info[instance] = valueelse:print('Negative value is not allowed')def __delete__(self, instance):print('delete')
这里创建了一个弱引用字典self.info
,每次赋值的时候会将不同的实例做为key存进去,取值的时候再以实例做为key获取,找不到的就返回默认值。
效果如下
if __name__ == '__main__':xiaofu = People()zhangsan = People()print('-' * 10)print(xiaofu.age) # 0print(zhangsan.age) # 0xiaofu.age = 100print(xiaofu.age) # 100print(zhangsan.age) # 0zhangsan.age=20print(xiaofu.age) # 100print(zhangsan.age) # 20
这样子基本在绝大多数场合都没问题了,除非遇上了无法被哈希的实例,例如list的之类。为了解决这个问题,python3.6开始又给描述符引入了一个新的方法__set_name__
def __set_name__(self, owner, name):pass
其中name
就是该描述符对象的名字。
再次修改下描述符类定义
class MyDescriptor:def __init__(self, default):self.val = defaultdef __get__(self, instance, owner):return instance.__dict__.get(self.name, self.val)def __set__(self, instance, value):if value >= 0:instance.__dict__[self.name] = valueelse:print('Negative value is not allowed')def __set_name__(self, owner, name):self.name = namedef __delete__(self, instance):print('delete')
通过对每个实例添加一个新的实例属性来达到目的。
扩展知识点
将描述符的基本使用和改进版本弄懂后,下面说几个扩展的知识点。
描述符添加额外方法
既然描述符也是一个类对象,当然也可以往里面添加自定义方法,这就为属性添加了额外接口。例如想要实现一个年龄转出生年份的接口就可以用类似的方法,这里就不演示了。
属性查找顺序
在描述符之前,我们可能只知道类属性和实例属性,并且同名的实例属性会覆盖类属性。但是引入了描述符之后,这个顺序会有点改变。
方法也是属性的一种,查找顺序对方法也有效
首先,不管是类还是类的对象,都会有各自的__dict__
属性,里面存放的是用户自定义的属性。同时描述符是在类下面定义的属性,所以只存在于类的__dict__
中。下面来看看描述符和实例属性哪个优先级比较高。
先来看非数据描述符
class MyDescriptor:def __init__(self, default):self.val = defaultdef __get__(self, instance, owner):return self.valclass People:age = MyDescriptor(0)if __name__ == '__main__':xiaofu = People()xiaofu.__dict__['age']=10print(xiaofu.age) # 10
可以看出,非数据描述符的优先级低过实例属性。
再来看看数据描述符
class MyDescriptor:def __init__(self, default):self.val = defaultdef __get__(self, instance, owner):return self.valdef __set__(self, instance, value):passdef __set_name__(self, owner, name):passdef __delete__(self, instance):passclass People:age = MyDescriptor(0)if __name__ == '__main__':xiaofu = People()xiaofu.__dict__['age']=10print(xiaofu.age) # 0
可见,数据描述符的优先级要高于实例属性。
所以我们可以重新整理一下在进行类似xiaofu.age
操作的时候,python是怎么去查询的
- 如果age是内置的属性,直接被找到
- 去类的
__dict__
中查找,如果查找到了age,并且是数据描述符,直接执行其中的__get__
方法。否则去父类的__dict__
中查找,一直往上 - 去实例的
__dict__
中查找 - 再去类的
__dict__
中查找,如果查找到了age(一定是非数据描述符),执行其__get__
方法。如果找到了普通属性直接返回 - 放弃查找,抛出异常
这是执行取值时候的顺序,赋值时候的优先级也类似。
函数也是描述符
上面说的都是属性,并没有提到方法。
方法就是一个函数,但是区别就在于会自动处理第一个self
参数。python中一切皆对象,函数也不例外,如果看一下函数类的定义,会发现也有一个__get__
方法
class Function(object):. . .def __get__(self, obj, objtype=None):"Simulate func_descr_get() in Objects/funcobject.c"if obj is None:return selfreturn types.MethodType(self, obj)
这说明函数本身也是描述符,并且当函数是类方法时,还利用types.MethodType()
将其绑定到了类的实例上。这也就是为什么类方法能自动处理self
的原因。
说这个主要是为后面装饰器在类方法上的使用做铺垫,因为一个类方法被类装饰器装饰以后就变成了对象,失去了函数的描述符特性,变得不能自动处理self
参数。此时就需要我们手动在类装饰器中定义__get__
方法完成这一过程。
我会在装饰器进阶的博客中详细说明,欢迎大家关注。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
python中@property以及描述符descriptor详解相关推荐
- python中换行的转义符_详解Python中的各种转义符\n\r\t
Python中的各种转义符\n\r\t 转义符 描述 \ 续行符(在行尾时) \\ 反斜杠符号 ' 单引号 " 双引号 \a 响铃 \b 退格(Backspace) \e 转义 \000 空 ...
- 技术图文:Python描述符 (descriptor) 详解
背景 今天在B站上学习"零基础入门学习Python"这门课程的第46讲"魔法方法:描述符",这也是我们组织的 Python基础刻意练习活动 的学习任务,其中有这 ...
- python描述符详解_Python描述符 (descriptor) 详解
1.什么是描述符? python描述符是一个"绑定行为"的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有 __get__(), __set__(), 和__de ...
- javascript描述符descriptor详解
一.描述符对象是个什么东西? javascript里,有时候不想让用户修改某个对象的属性,则可以把这个对象的属性设置为不可写的,这样用户就不能对该属性进行修改了. 实际操作为: 这样看来,用户修改属性 ...
- python中时间戳、字符串之间转换详解
[转载]python中时间戳.字符串之间转换详解 (2013-04-30 17:36:07) 转载▼ 标签: 转载 原文地址:python中时间戳.字符串之间转换详解作者:doris0920 1)秒数 ...
- python gil 解除_详解Python中的GIL(全局解释器锁)详解及解决GIL的几种方案
先看一道GIL面试题: 描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因. GIL:又叫全局解 ...
- 站长在线Python精讲:在Python中函数的定义与创建详解
欢迎你来到站长在线的站长学堂学习Python知识,本文学习的是<在Python中函数的定义与创建详解>.本文的主要内容有:函数的定义.函数的定义规则.函数的创建. 目录 1.函数的定义 2 ...
- 站长在线python精讲:在Python中使用“+”运算符来拼接字符串详解
欢迎你来到站长在线的站长学堂学习Python知识,本文学习的是<在Python中使用"+"运算符来拼接字符串详解>.本知识点主要内容有:在Python中,我们可以使用& ...
- python argv 详解_对python中的argv和argc使用详解
主要问题 为什么argv中第一个,即index=0的内容就是文件名? python中argc是用什么实现的? 概念解释 argc:argument counter,命令行参数个数 argv:argum ...
最新文章
- SRM6.1安装配置指南
- 浅析ios开发中Block块语法的妙用
- Javascript中document.execCommand()的用法
- (4)计数器systemverilog与VHDL编码
- 我又发现一个直接就能安装中文小红帽的方法
- IOS --xcode删除Provisioning Profiles文件
- Linux文件权限管理命令
- DIY无人机组装与飞控参数调试记录(DJI NAZA-LITE)
- 手写平衡二叉树(二)
- MT7620A的DTS
- 解决安装虚拟机vmware无法打开注册表项的问题
- 深入浅出剖析JAVA多线程原理
- java计算机毕业设计ssm基于SSM学生信息管理系统37myx(附源码、数据库)
- 学习信奥要不要先学python
- IgH详解六、IgH命令行工具使用
- Legacy引导转UEFI引导(BIOS、Legacy引导、UEFI引导、GPT/MBR分区)
- 高德地图根绝经纬度画线跑步软件
- 知识体系更新迭代的探索
- Spring 01 初识 Spring
- glibc detected *** double free 错误解决方法