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是怎么去查询的

  1. 如果age是内置的属性,直接被找到
  2. 去类的__dict__中查找,如果查找到了age,并且是数据描述符,直接执行其中的__get__方法。否则去父类的__dict__中查找,一直往上
  3. 去实例的__dict__中查找
  4. 再去类的__dict__中查找,如果查找到了age(一定是非数据描述符),执行其__get__方法。如果找到了普通属性直接返回
  5. 放弃查找,抛出异常

这是执行取值时候的顺序,赋值时候的优先级也类似。

函数也是描述符

上面说的都是属性,并没有提到方法。

方法就是一个函数,但是区别就在于会自动处理第一个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详解相关推荐

  1. python中换行的转义符_详解Python中的各种转义符\n\r\t

    Python中的各种转义符\n\r\t 转义符 描述 \ 续行符(在行尾时) \\ 反斜杠符号 ' 单引号 " 双引号 \a 响铃 \b 退格(Backspace) \e 转义 \000 空 ...

  2. 技术图文:Python描述符 (descriptor) 详解

    背景 今天在B站上学习"零基础入门学习Python"这门课程的第46讲"魔法方法:描述符",这也是我们组织的 Python基础刻意练习活动 的学习任务,其中有这 ...

  3. python描述符详解_Python描述符 (descriptor) 详解

    1.什么是描述符? python描述符是一个"绑定行为"的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有 __get__(), __set__(), 和__de ...

  4. javascript描述符descriptor详解

    一.描述符对象是个什么东西? javascript里,有时候不想让用户修改某个对象的属性,则可以把这个对象的属性设置为不可写的,这样用户就不能对该属性进行修改了. 实际操作为: 这样看来,用户修改属性 ...

  5. python中时间戳、字符串之间转换详解

    [转载]python中时间戳.字符串之间转换详解 (2013-04-30 17:36:07) 转载▼ 标签: 转载 原文地址:python中时间戳.字符串之间转换详解作者:doris0920 1)秒数 ...

  6. python gil 解除_详解Python中的GIL(全局解释器锁)详解及解决GIL的几种方案

    先看一道GIL面试题: 描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因. GIL:又叫全局解 ...

  7. 站长在线Python精讲:在Python中函数的定义与创建详解

    欢迎你来到站长在线的站长学堂学习Python知识,本文学习的是<在Python中函数的定义与创建详解>.本文的主要内容有:函数的定义.函数的定义规则.函数的创建. 目录 1.函数的定义 2 ...

  8. 站长在线python精讲:在Python中使用“+”运算符来拼接字符串详解

    欢迎你来到站长在线的站长学堂学习Python知识,本文学习的是<在Python中使用"+"运算符来拼接字符串详解>.本知识点主要内容有:在Python中,我们可以使用& ...

  9. python argv 详解_对python中的argv和argc使用详解

    主要问题 为什么argv中第一个,即index=0的内容就是文件名? python中argc是用什么实现的? 概念解释 argc:argument counter,命令行参数个数 argv:argum ...

最新文章

  1. SRM6.1安装配置指南
  2. 浅析ios开发中Block块语法的妙用
  3. Javascript中document.execCommand()的用法
  4. (4)计数器systemverilog与VHDL编码
  5. 我又发现一个直接就能安装中文小红帽的方法
  6. IOS --xcode删除Provisioning Profiles文件
  7. Linux文件权限管理命令
  8. DIY无人机组装与飞控参数调试记录(DJI NAZA-LITE)
  9. 手写平衡二叉树(二)
  10. MT7620A的DTS
  11. 解决安装虚拟机vmware无法打开注册表项的问题
  12. 深入浅出剖析JAVA多线程原理
  13. java计算机毕业设计ssm基于SSM学生信息管理系统37myx(附源码、数据库)
  14. 学习信奥要不要先学python
  15. IgH详解六、IgH命令行工具使用
  16. Legacy引导转UEFI引导(BIOS、Legacy引导、UEFI引导、GPT/MBR分区)
  17. 高德地图根绝经纬度画线跑步软件
  18. 知识体系更新迭代的探索
  19. Spring 01 初识 Spring
  20. glibc detected *** double free 错误解决方法

热门文章

  1. 2022年下半年网络规划设计师考试下午真题
  2. 基于QT的网络视频会议系统---KNVM
  3. 写在28岁,回看3年前的自己,庆幸当时入了软件测试这行
  4. 【毕业设计 大作业高分项目】html+php实现个人博客网站
  5. iOS 手机 邮箱 正则表达式
  6. 云付董事长 张凉凉:光环背后的“执拗者”
  7. 简洁风个人主页(3) js背景图片随机切换
  8. 江苏小学计算机面试题目,2019下半年江苏省小学信息技术教师资格证面试试题(精选)(四)...
  9. Pots --bfs
  10. PAT 乙级 1086 python