Descriptor基础

python中的描述符可以用来定义触发自动执行的代码,它像是一个对象属性操作(访问、赋值、删除)的代理类一样。前面介绍过的property是描述符的一种。

大致流程是这样的:

  1. 定义一个描述符类D,其内包含一个或多个__get__()__set__()__delete__()方法
  2. 将描述符类D的实例对象d赋值给另一个要代理的类中某个属性attr,即attr = D()
  3. 之后访问、赋值、删除attr属性,将会自动触发描述符类中的__get__()__set__()__delete__()方法

简言之,就是创建一个描述符类,它的实例对象作为另一个类的属性

要定义描述符类很简单,只要某个类中包含了下面一个或多个方法,就算是满足描述符协议,就是描述符类,就可以作为属性操作的代理器。

class Descriptor():
def __get__(self, instance, owner):...
def __set__(self, instance, value):...
def __delete__(self, instance):...

需要注意的是,__get__的返回值需要是属性值或抛异常,另外两个方法要返回None。

还需注意的是不要把__delete____del__搞混了,前者是实现描述符协议的一个方法,后者是对象销毁函数(也常称为析构函数)。

先不管这几个方法中的参数,看一个示例先:

class Descriptor():
def __get__(self, instance, owner):
print("self: %s\ninstance: %s\nowner: %s" % (self, instance, owner)) class S:
# 描述符的示例对象作为S的属性
attr = Descriptor() s1 = S()
s1.attr # 访问对象属性 print("-" * 30)
S.attr # 访问类属性

输出结果:

self: <__main__.Descriptor object at 0x030C02D0>
instance: <__main__.S object at 0x030C0AB0>
owner: <class '__main__.S'>
------------------------------
self: <__main__.Descriptor object at 0x030C02D0>
instance: None
owner: <class '__main__.S'>

不难看出,在访问类S中的属性attr时,表示访问描述符类的实例对象,它会自动调用描述符类中的__get__方法。

在这个方法中,3个参数self、instance、owner分别对应的内容从结果中已经显示出来了。它们之间有以下等价关系:

s1.attr -> Descriptor.__get__(S.attr, s1, S)
S.attr -> Descriptor.__get__(S.attr, None, S)

所以,这里解释下__get__(self, instance, owner)中的三个参数:

  • self:描述符对象自身,也就是被代理类S中的属性attr
  • instance:被代理类的实例对象。所以访问类属性(class.attr)时为None
  • owner:将描述符对象附加到哪个类上,其实是instance所属的类,也就是type(instance)

再解释下这里相关的几个角色:

  • Descriptor:是描述符类,也是代理者
  • S:是另一个类,是托管类、客户类,也就是参数中的owner
  • attr = Descriptor():是描述符的实例对象,attr是托管类的属性,也就参数中的self
  • s1:是托管类实例对象,也就是参数中的instance

按照descriptor的功能,大概可以用上面的方式去定义各个角色。当然,角色的定义没什么限制。

descriptor的作用发挥在哪

当定义了一个类后,可以访问、赋值、删除它的属性,这些操作也同样适用于它的实例对象。

例如Foo类:

class Foo():
... f = Foo()
a = f.bar # 访问属性
f.bar = b # 赋值属性
del f.bar # 删除属性

decriptor发挥作用的时候就在于执行这3类操作的时候:

  • 当访问x.d的时候,将自动调用描述符类中的__get__
  • 当赋值x.d的时候,将自动调用描述符类中的__set__
  • 当删除x.d的时候,将自动调用描述符类中的__delete__

考虑一下:如果x所属的类中已经定义了__getattr____setattr____delattr__会如何,是描述符类中的先生效,还是x自身所属类的这几个方法会生效。再继续考虑,如果x所属类没有定义,但它的父类定义了这几个方法,谁会生效。可自行测试或者参考我的下一篇文章。

示例1:原始代码

假设现在有一个Student类,需要记录stuid、name、score1、score2、score3信息。

class Student():
def __init__(self, stuid, name, score1, score2, score3):
self.stuid = stuid
self.name = name
self.score1 = score1
self.score2 = score2
self.score3 = score3 def returnMe(self):
return "%s, %s, %i, %i, %i" % (
self.stuid,
self.name,
self.score1,
self.score2,
self.score3) stu = Student("20101120", "malong", 67, 77, 88)
print(stu.returnMe())

但是现在有个需求,要求score1-score3的数值范围只能是0-100分。

于是修改__init__()

class Student():
def __init__(self, stuid, name, score1, score2, score3):
self.stuid = stuid
self.name = name if 0 <= score1 <= 100:
self.score1 = score1
else:
raise ValueError("score not in [0,100]") if 0 <= score2 <= 100:
self.score2 = score2
else:
raise ValueError("score not in [0,100]") if 0 <= score3 <= 100:
self.score3 = score3
else:
raise ValueError("score not in [0,100]")

这个修改对于初始化Student对象时有效,但Python中属性的赋值太过自由,之后可以随意赋值:

stu = Student("20101120", "malong", 67, 77, 88)

stu.score1 = -23
print(stu.returnMe())

使用property

使用Property或者自定义的getter、setter或运算符__getattr____setattr__重载都能解决上面的问题,保证无法赋值超出0到100范围内的数值。

class Student():
def __init__(self, stuid, name, score1, score2, score3):
self.stuid = stuid
self.name = name
self._score1 = score1
self._score2 = score2
self._score3 = score3 def get_score1(self):
return self._score1 def set_score1(self, score):
if 0 <= score <= 100:
self._score1 = score
else:
raise ValueError("score not in [0,100]") def get_score2(self):
return self._score2 def set_score2(self, score):
if 0 <= score <= 100:
self._score2 = score
else:
raise ValueError("score not in [0,100]") def get_score3(self):
return self._score3 def set_score3(self, score):
if 0 <= score <= 100:
self._score3 = score
else:
raise ValueError("score not in [0,100]") score1 = property(get_score1, set_score1)
score2 = property(get_score2, set_score2)
score3 = property(get_score3, set_score3) def returnMe(self):
return "%s, %s, %i, %i, %i" % (
self.stuid,
self.name,
self.score1,
self.score2,
self.score3)

下面测试时将抛出异常。

stu = Student("20101120", "malong", 67, 77, 88)
print(stu.returnMe())
stu.score1 = -23

但很显然,上面的重复代码太多了。

使用descriptor

如果使用descriptor,将很容易解决上面的问题。只需将score1、score2、score3交给描述符类托管即可。

from weakref import WeakKeyDictionary

class Score():
""" score should in [0,100] """ def __init__(self):
self.score = WeakKeyDictionary()
#self.score = {} def __get__(self, instance, owner):
return self.score[instance] def __set__(self, instance, value):
if 0 <= value <= 100:
self.score[instance] = value
else:
raise ValueError("score not in [0,100]") class Student():
# 托管属性定义在类级别上
score1 = Score()
score2 = Score()
score3 = Score() def __init__(self, stuid, name, score1, score2, score3):
self.stuid = stuid
self.name = name
self.score1 = score1
self.score2 = score2
self.score3 = score3 def returnMe(self):
return "%s, %s, %i, %i, %i" % (
self.stuid,
self.name,
self.score1,
self.score2,
self.score3) stu = Student("20101120", "malong", 67, 77, 88)
print(stu.returnMe())
stu.score1 = -23

很明显地,它们的代码被完整地复用了。这里score1、score2、score3被描述符类Score托管了,这3个分值分别被放进了Score实例对象的dict中(是单独存放它们还是使用dict数据结构来保存,取决于你)。

另外,上面使用了弱引用的字典,因为每个属性只在描述符对象中才会被用上,为了保证Student对象被销毁的时候能释放这些资源,所以采用弱引用,避免出现内存泄漏。

参考资料

python理解描述符(descriptor)的更多相关文章

  1. 【python】描述符descriptor

    开始看官方文档,各种看不懂,只看到一句Properties, bound and unbound methods, static methods, and class methods are all ...

  2. python tips:描述符descriptor

    描述符(descriptor)是实现了__get__.__set__.__del__方法的类,进一步可以细分为两类: 数据描述符:实现了__get__和__set__ 非数据描述符:没有实现__set ...

  3. Python 描述符(descriptor) 杂记

    转自:https://blog.tonyseek.com/post/notes-about-python-descriptor/ Python 引入的“描述符”(descriptor)语法特性真的很黄 ...

  4. python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解

     1.前言 Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题 ...

  5. python描述符descriptor(一)

    Python 描述符是一种创建托管属性的方法.每当一个属性被查询时,一个动作就会发生.这个动作默认是get,set或者delete.不过,有时候某个应用可能会有 更多的需求,需要你设计一些更复杂的动作 ...

  6. python数据描述符

    Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过google和阅读源码,现将自己的理解和心得记录下来,也为正在为了该问题苦恼的朋友提 ...

  7. python描述符 descriptor

    descriptor 在python中,如果一个新式类定义了__get__, __set__, __delete__方法中的一个或者多个,那么称之为descriptor.descriptor通常用来改 ...

  8. Python描述符 (descriptor) 详解

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

  9. Python 描述符(Descriptor) 附实例

    在 Python 众多原生特性中,描述符可能是最少被自定义的特性之一,但它在底层实现的方法和属性却无时不刻被使用着,它优雅的实现方式体现出 Python 简洁之美. 定义 一个描述符是一个有" ...

随机推荐

  1. thinkphp 把小程序码二进制流存储到本地

    public function getxcxm(){ $id = input('id'); $astk = json_decode($this->getasstk())->access_t ...

  2. 实践中 XunSearch(讯搜)的使用教程步骤

    XunSearch(讯搜)的使用教程步骤 一.安装编译工具 yum install make gcc g++ gcc-c++ libtool autoconf automake imake mysql ...

  3. nice coding (与其亡羊补牢,不如未雨绸缪)

    一.需求前 架构规范 建模规范 编码规范(流程控制,方法抽取,日志打印等) <Effective Java> <Design Patterns> 二.需求中 1. 明确需求(别 ...

  4. android hal 诠释

    历史原因使Android系统有了HAL,它的角色相当于一个中间人,对上层,它负责给JNI提供调用kernel的方法,对下层,它所提供的方法包含能够访问kernel的函数,即kernel提供给上层的AP ...

  5. submit插件安装的问题与集成了插件的submit

    写在最前面,方法有二种.一种是在线安装,这种办法我尝试过,受网速和软件卡顿的问题,进行不顺利.第二种就是我下面介绍的这种,手动安装. 最精华的在后面,可以直接跳转到最后.我找了很久的,最新版的汉化,而 ...

  6. Java-异常初步练习

    案例一: package com.esandinfo; /** * 自定义一个Exception类 */ class MyCustomException extends RuntimeExceptio ...

  7. 协程 及 libco 介绍

    libco 是腾讯开源的一个协程库,主要应用于微信后台RPC框架,下面我们从为什么使用协程.如何实现协程.libco使用等方面了解协程和libco. why协程 为什么使用协程,我们先从server框 ...

  8. Java安全编码:糟糕的在线建议和令人困惑的APIs

    对于程序员和软件开发人员来说,网络论坛提供了一个交流知识和寻找具体编码难题答案的好地方.遗憾的是,他们并不总是准确信息的来源. 弗吉尼亚理工大学的一组研究人员分析了数百篇关于Stack Overflo ...

  9. js怎么实现继承?

    3. js怎么实现继承? 1. 使用原型prototype 这个问题其实之前总结过了……但是面试时候有点忘……主要思想是记得的,但是不会写,还是基础太不牢靠,写的太少了.一开始因为不知道怎么能继承父类 ...

  10. 【CF429E】 Points and Segments(欧拉回路)

    传送门 CodeForces 洛谷 Solution 考虑欧拉回路有一个性质. 如果把点抽出来搞成一条直线,路径看成区间覆盖,那么一个点从左往右被覆盖的次数等于从右往左被覆盖的次数. 发现这个性质和本 ...