Python描述符

作者: Rethink | 来源:发表于2019-05-03 20:51 被阅读0次

Descriptor Any object which defines the methods get(), set(), or delete(). When a class attribute is a descriptor, its special binding behavior is triggered upon attribute lookup. Normally, using a.b to get, set or delete an attribute looks up the object named b in the class dictionary for a, but if b is a descriptor, the respective descriptor method gets called.

从表现形式来,一个对象如果实现了__get__, __set__, __del__方法(三个方法不一定要全都实现),那么这个对象就是一个描述符。__get__, __set__, __del__的具体声明如下:

# 用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常
__get__(self,instance,owner)
​
# 在属性分配操作中调用,不会返回任何内容
__set__(self,instance,value)
​
# 控制删除操作,不会返回内容
__del__(self,instance)

Python是动态类型解释性语言,不像C/C++等静态编译型语言,数据类型在编译时便可以进行验证。在Python中必须添加额外的类型检查代码才能做到这一点,这就是描述符的初衷。初次之外描述符还有诸多其他优点,如保护属性不受修改和自动更新依赖属性的值等。

比如,现在有一个Student类:

 def __init__(self, name, math, chinese, english):
 self.name = name
 self.math = math
 self.chinese = chinese
 self.english = english
​
 def __repr__(self):
 return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
 self.name, self.math, self.chinese, self.english
 )
​
stu1 = Student('Alex', 66, 77, 88)
print(stu1)
>>>
<Student: Alex, math:66, chinese: 77, english: 88>

看起来一切都很顺利,但是代码并不像人那么智能,不会根据实际使用场景自动判断数据的合法性,比如在录入会员某门成绩时,不小心多输入了一位或者根本就是不合法的数字,程序是无法感知的,所以我们还需要在代码中加入判断逻辑,完善后代码如下:

 def __init__(self, name, math, chinese, english):
 self.name = name
 self.math = math
 self.chinese = chinese
 self.english = english
​
 @property
 def math(self):
 return self._math
​
 @math.setter
 def math(self, score):
 if 0 <= score <= 100:
 self._math = score
 else:
 raise ValueError("Valid score must be in [0, 100]")
​
 @property
 def chinese(self):
 return self._chinese
​
 @chinese.setter
 def chinese(self, score):
 if 0 <= score <= 100:
 self._chinese = score
 else:
 raise ValueError("Valid score must be in [0, 100]")
​
 @property
 def english(self):
 return self._english
​
 @english.setter
 def english(self, score):
 if 0 <= score <= 100:
 self._english = score
 else:
 raise ValueError("Valid score must be in [0, 100]")
​
 def __repr__(self):
 return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
 self.name, self.math, self.chinese, self.english
 )
​
​
stu1 = Student('Alex', 666, 77, 88)
print(stu1)
​
>>>
ValueError: Valid score must be in [0, 100]

在上面的代码中,使用了 property 特性,把函数调用伪装成对属性的调用,并对对属性的合法性进行了有效控制,从功能上来说没有问题,但是重复代码率非常高,如果有更多个属性需要做判断,那么代码就会变得更加冗长,虽然property可以让类从外部看起来整洁漂亮,但是却做不到内部同样整洁漂亮。这个时候,描述符就要上场了,它是property的升级版,允许我们为重复的property逻辑编写单独的类来进行处理。

不要尝试在__int__ 方法中使用 if ..else的方法来对属性进行控制,这对于已经存在的实例对象毫无帮助。

使用描述符,对上面的代码进行重写:

class Score:
 def __init__(self, default=0):
 self._score = default
​
 def __set__(self, instance, value):
 if not isinstance(value, int):
 raise TypeError('Score must be integer')
 if not 0 <= value <= 100:
 raise ValueError('Valid score must be in [0, 100]')
​
 self._score = value
​
 def __get__(self, instance, owner):
 return self._score
​
 def __del__(self):
 del self._score
​
​
class Student:
 math = Score()
 chinese = Score()
 english = Score()
​
 def __init__(self, name, math, chinese, english):
 self.name = name
 self.math = math
 self.chinese = chinese
 self.english = english
​
 def __repr__(self):
 return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
 self.name, self.math, self.chinese, self.english
 )
​
​
stu1 = Student('Alex', 66, 77, 88)
print(stu1)

如上所述,Score 类就是一个描述符对象,当从 Student 的实例访问 math、chinese、english这三个属性的时候,都会经过 Score 类里的特殊的方法,由描述符对象为我们做合法性检查,这就避免了使用property 时出现的大量代码无法复用的问题。

在使用描述符时,为了让描述符能够正常工作,它们必须定义在类的层次上。如果不这么做,那么Python将无法自动调用__get____set__方法。如下:

class Test():
 t1 = Score(20)
​
 def __init__(self):
 self.t2 = Score(20)
​

运行结果>>>
20 <__main__.Score object at 0x10d0987f0>

可以看到,访问类层次上的描述符t1可以自动调用__get__ ,但是访问实例层次上的描述符 t2 只会返回描述符本身!

确保实例的数据只属于实例本身

看下面的例子:

 t1 = Score(20)
​
 def __init__(self):
 self.t2 = Score(20)
​
​
class Rethink():
 math = Score(80)
​
​
me = Rethink()
Alex = Rethink()
print(me.math, Alex.math)
me.math=90
print(me.math, Alex.math)
​
>>>
80 80
90 90

运行结果出人意料,这说明所有的Test对象的实例竟然都共享相同的math属性!这简直让人无法接受,它也是描述符中最令人感到别扭的地方。为了解决这个问题,我们需要在描述符对象中使用数据字典,__get____set__的第一个参数告诉我们需要关心哪一个实例,描述符使用这个参数作为字典的key,为每一个实例单独保存一份数据。修改后的Score类的代码如下:

from weakref import WeakKeyDictionary
​
​
class Score:
 def __init__(self, default=0):
 self._score = default
 self.data = WeakKeyDictionary()
​
 def __set__(self, instance, value):
 if not isinstance(value, int):
 raise TypeError('Score must be integer')
 if not 0 <= value <= 100:
 raise ValueError('Valid score must be in [0, 100]')
​
 self.data[instance] = value
​
 def __get__(self, instance, owner):
 return self.data.get(instance, self._score)
​
​
class Rethink():
 math = Score(80)
​
me = Rethink()
Alex = Rethink()
print(me.math, Alex.math)
me.math=90
print(me.math, Alex.math)
​
>>>
80 80
90 80

[To be continued...]

参考文档

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

  2. 描述符:其实你不懂我(一),公众号:Python编程时光

  3. 解密 Python 的描述符(descriptor)

相关文章

网友评论

    本文标题:Python描述符

    本文链接:https://www.haomeiwen.com/subject/dfgznqtx.html