本文声明:python的描述符descriptor,这是属于python高级编程的一些概念和实现方法,可能有很多的小伙伴还并没有用到过,但是在Python的面试过程中有可能会出现,究竟什么是python描述符,有什么作用,使用有什么意义,它的诞生背景是什么,很少有文章专门介绍这一块,有的文章介绍的太过粗浅,以至于看过之后依然不能够理解描述符的本质。鉴于此,我寻思着出一期专门讲解python描述符的系列文章,跟前面的python装饰器系列文章一样,因为涉及到的内容偏多,本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第三篇——下篇,介绍Python的描述符、描述符协议、描述符三剑客、描述符的详细实现等。
前面饶了很多弯子,一步一步引入属性访问的优先级顺序这样一个主题,然后是属性控制的三剑客,似乎还是和描述符descriptor没啥关系啊,关系自然是有的。本文会一一说明,首先我将一系列的概念和定义一次性的写出来,后面再加以分析说明。
1、什么是描述符——descriptor以及相关的一系列定义
(1)描述符:某个类,只要是内部定义了方法 __get__, __set__, __delete__ 中的一个或多个,就可以称为描述符,描述符的本质是一个类。
(2)描述符协议:描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一个,这些魔术方法也被称为描述符协议
(3)非数据描述符:一个类,如果只定义了 __get__() 或者是__delete__()方法,而没有定义 __set__()方法,则认为是非数据描述符(即没有定义__set__)
(4)数据描述符:一个类,不仅定义了 __get__() 方法,还定义 __set__(), __delete__() 方法,则认为是数据描述符(即定义了__get__和__set__)
(5)描述符对象:描述符(即一个类,因为描述符的本质是类)的一个对象,一般是作为其他类对象的属性而存在
2、描述符的作用
描述符就是一个“绑定行为“的对象属性,在描述符协议中,它可以通过方法充写属性的访问。我们或许经常看见描述符的作用描述中,有两个关键词“绑定行为”和“托管属性”,那到底是什么意思呢,我给出一个通俗的解释,
绑定行为:所谓的绑定行为,是指在属性的访问、赋值、删除时还绑定发生了其他的事情,正如前面属性控制三剑客所完成的事情一样;
托管属性:python描述符是一种创建“托管属性”的方法,即通过描述符(类)去托管另一个类的相关属性,也可以说是类的属性的一个代理。为了方便的理解“托管属性”这个概念,将通过几个通俗的例子去说明。
以人类而言,Person是一个类,人应该有很多属性,比如人是美丽的、性感的、成熟的、博学的、大方的等等,所谓的“描述”,本身指的就是描述某一个类的某一些特性的,在程序设计中,属性就是用来描述类的特征的,所谓的描述符(描述类)就是专门再创建一个类,让这个类去描述本身那个类的相关属性,这也正是“描述”二字的由来,其实和我们生活中的描述是一个意思。
描述符的作用是用来代理另外一个类的属性的
后面的代码也将从“绑定行为”和“托管属性”两个方面进行说明。
3、描述符三个函数的定义形式:
def __get__(self, instance, owner)
self:指的是描述符类的实例
instance:指的是使用描述符的那个类的实例,如student。下面的instance一样的意思。
owner:指的是使用描述符的那个类,如Student
def __set__(self, instance, value)
def __delete__(self, instance,)
前面讲了,要实现所谓的描述符,就是要实现上面的三个魔术方法,但是和普通类定义的方式不一样,因为“属性代理(属性托管)”的机制,我们需要定一两个类,一个类A,一个ADescriptor类,即所谓的描述类。
注意,不是直接在一个类中定义上面的描述符的三个方法哦!
1、从一个简单的实例说起——认识描述符
#人的性格描述,悲观的?开朗的?敏感的?多疑的?活泼的?等等
class CharacterDescriptor:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
print("访问性格属性")
return self.value
def __set__(self, instance, value):
print("设置性格属性值")
self.value = value
#人的体重描述,超重?过重?肥胖?微胖?合适?偏轻?太瘦?等等
class WeightDescriptor:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
print("访问体重属性")
return self.value
def __set__(self, instance, value):
print("设置体重属性值")
self.value = value
class Person:
character=CharacterDescriptor('乐观的')
weight=WeightDescriptor(150)
p=Person()
print(p.character)
print(p.weight)
运行结果为:
访问性格属性
乐观的
访问体重属性
150
先不管运行结果,我们仅仅针对上面的代码,发现一个问题,现在明白为什么称描述符为“属性代理”了吧,他其实就是专门用一个类去装饰某一个属性,我可以把这个属性定义成任何我想要的样子,所谓的“一对一定制属性”。人有体重和性格这两个属性,当然我可以把这两个属性都定义在Person类里面,但是这就不方便为这个属性的操作绑定相应的行为,进行任意的个性化定制属性了,你也许会说,我依然可以通过“属性控制三剑客”完成啊,参见上一篇文章:
python高级编程——描述符Descriptor详解(中篇)——python对象的属性访问优先级与属性的控制与访问)
但是“属性控制三剑客”的缺点就是无法“一对一定制”,他虽然可以为属性绑定行为,但是任何属性都会绑定,不太方面将一个属性定制成任意我想要的样子。
再仔细一看,实际上完成了不就是Person的一个类属性本质上就是属性描述类的一个实例对象啊!哦,原来如此,的确如此,但是需要注意的是,在访问Person的这个类属性的时候,会发生一些特别的事情。因为我们发现,我们打印的print(p.character)中的character应该是CharacterDescriptor类的实例对象,为什么会打印出一个具体的值呢?这是因为:
访问Person的character属性时,调用了描述符CharacterDescriptor类的__get__()
方法。这就达到了描述符的作用。
总结:对于类属性描述符,如果解析器发现属性property是一个描述符的话,它能把Class.x
转换成Class.__dict__[‘property’].__get__(None, Class)
来访问。
2、类属性描述符
依然用上面的代码,只是下面添加以下几句话。
p=Person()
print(p.character) #属性的访问
print(p.weight) #
p.weight=200 #修改属性
print(p.weight)
del p.weight #删除属性
print(p.weight)
运行结果为:
访问性格属性
乐观的
访问体重属性
150
设置体重属性值
访问体重属性
200
删除体重属性
访问体重属性
Traceback (most recent call last):显示AttributeError: 'WeightDescriptor' object has no attribute 'value'
总结:
(1)对于类装饰器属性,只要出现属性访问(不管是通过对象访问还是类名访问),都会优先调用装饰器的__get__方法;
(2)对于类装饰器属性,若出现属性修改(不管是通过对象访问还是类名访问),都会优先调用装饰器的__set__方法;
(3)对于类装饰器属性,若出现属性删除(不管是通过对象访问还是类名访问),都会优先调用装饰器的__delete__方法;
3、实例属性描述符
两个描述符类的代码不变,仅仅改变Person类的代码,如下:
class Person:
def __init__(self):
self.character=CharacterDescriptor('乐观的')
self.weight=WeightDescriptor(150)
p=Person()
print(p.character) #属性的访问
print(p.weight) #
p.weight=200 #修改属性
print(p.weight)
del p.weight #删除属性
print(p.weight)
运行结果为:
<__main__.CharacterDescriptor object at 0x000001963C643780>
<__main__.WeightDescriptor object at 0x000001963C6437B8>
200
Traceback (most recent call last):AttributeError: 'Person' object has no attribute 'weight'
为什么?
并没有像我们预期的那样调用__get__()、__set__()、__delete__()
方法,只是说他是Descriptor的一个对象。
总结:描述符是一个类属性,必须定义在类的层次上, 而不能单纯的定义为对象属性。
通过上面的这几个例子,现在应该可以好好体会到“描述符”的两个层面的作用了:
绑定行为:在访问雷属性的时候,会打印出很多的额外信息,这不就是在添加额外的行为吗?
属性代理(托管属性):将某个属性专门用一个描述符(描述类)加以托管,实现任意的定制化,一对一的定制属性。
3、类属性描述符对象和实例属性同名时
前面说了,描述符针对的是类属性,但是当一个类中,如果类属性是描述符对象,而实例属性由于这个描述符属性同名,这该怎么办呢?
class Person:
character=CharacterDescriptor('乐观的')
weight=WeightDescriptor(150)
def __init__(self,character,weight):
self.character=character
self.weight=weight
p=Person('悲观的',200)
print(p.character) #属性的访问
print(p.weight) #
运行结果为:
设置性格属性值
设置体重属性值
访问性格属性
悲观的
访问体重属性
200
从上面的运行结果可以看出,首先是访问了描述符的__set__方法,这是因为在构建对象的时候,相当于为character和weight赋值,然后再调用__get__方法,这是因为访问了类属性character和weight,但是最终打印出来值却并不是类属性的值,这是因为,实例属性实际上是在“描述符类属性”后面访问的,所以覆盖掉了。
总结:到目前为止,我们接触到的属性有很多了,实例属性,类属性、描述符类属性、父类的类属性、带有属性控制函数三剑客的属性等,那么当一个属性同名的时候,访问的优先级到底是什么样子呢?
1、如果没有设置“描述符属性”
没有设置描述符属性,则属性的优先访问顺序和我们前面文章里面所讲的是一样的,即
(1) __getattribute__(), 无条件调用,任何时候都先调用
(2)实例属性
(3)类属性
(4)父类属性
(5) __getattr__() 方法 #如果所有的属性都没有搜索到,则才会调用该函数
2、如果设置了“描述符属性”
注意:因为描述符属性本身就是定义在类里面的,也可以当成是类属性,但是它并不是一般的类属性,请记住一句话:
一旦一个属性被标记为“描述符属性”,那它的性质再也不会变,与它同名的变量不管是放在类
(1)先比较实例属性和描述符属性
class Person:
a2=CharacterDescriptor('乐观的')
def __init__(self):
self.a2='悲观的'
def __getattribute__(self,key):
print('__getattribute__')
return super(Person,self).__getattribute__(key)
def __getattr__(self,key):
print('__getattr__')
p=Person()
print(p.a2)
运行结果是:
设置性格属性值
__getattribute__
访问性格属性
悲观的
为什么会得到这样的结果?
第一句:设置性格属性值 :这是由p=Person()得到的,因为他会告诉你这是再给一个“描述符变量赋值,赋值为“悲观的”,所以调用了__set__”
后面三句:__getattribute__总是优先访问,而且访问的由于是“描述符变量”,故而访问的时候调用__get__
(2)类属性与描述符属性
class Person:
a2=CharacterDescriptor('乐观的')
a2='沮丧的'
def __init__(self):
pass
def __getattribute__(self,key):
print('__getattribute__')
return super(Person,self).__getattribute__(key)
def __getattr__(self,key):
print('__getattr__')
p=Person()
print(p.a2)
运行结果为:
__getattribute__
沮丧的
但是,这并不意味着类属性a2,就比描述符属性a2的优先级更高,仅仅是因为后面重新对a2进行复制,改变了a2的性质,不再是数据描述符,如果我交换两个a2的顺序,得到的结果为如下:
__getattribute__
访问性格属性
乐观的
因为此时,a2作为数据描述符存在。
3、疑惑不解
我搜集了很多博文,看到很多博主得到了如下结论,导致我自己也没有得出一个确切的定论,所以希望再次与广大网友讨论,下面的两个结论都是从博客上摘录下来的。
(1)类属性 > 数据描述符 > 实例属性 > 非数据描述符 > 找不到的属性触发__getattr__()
这样的说法显然不严谨,因为类属性不总是优先于实例属性的
(2) __getattribute__()> 数据描述符> 实例对象的字典(若与描述符对象同名,会被覆盖哦)>类的字典>非数据描述符
>父类的字典>__getattr__() 方法
这样的说法也不严谨,因为从我上面的调试来看,当数据描述符属性与实力属性同名的时候,最终显示的值是实例属性的值,但是并不是实例属性覆盖了描述符属性,恰好相反,此时,实例属性也是当做描述属性那样去用的,而且调用了__get__和__set__方法。
总结:个人认为“描述符”的作用有其特殊性,它的目的并不是改变属性访问的优先级,根本目的只是改变属性的控制方式,方便对属性进行更好的访问、修改和删除,所以没必要死记硬背一个固定的优先级,在具体的问题中根据代码的运行能够做出合理的判断即可。当然如果哪一位小伙伴有更加权威的排序,也可以私下里告诉我哦,解答我心中的疑惑,将万分感谢!
描述符的本质在于“描述”二字,最大的用处是对属性的个性定制与控制,如前所说,
(1)可以在设置属性时,做些检测等方面的处理
(2)设置属性不能被删除?那定义_delete_方法,并raise 异常。
(3)还可以设置只读属性
(4)把一个描述符作为某个对象的属性。这个属性要更改,比如增加判断,或变得更复杂的时候,所有的处理只要在描述符中操作就行了。
这一系列其实都是为了更好地去控制一个属性。
但是描述符因为它非常灵活的语法,可以实现一些非常高级的python特性,描述符是可以实现大部分python类特性中的底层魔法,包括@classmethod,@staticmethd,@property甚至是__slots__属性,不仅如此,描述父是很多高级库和框架的重要工具之一,描述符通常是使用到装饰器或者元类的大型框架中的一个组件。
作为python使用者,可能绝大部分使用者对于描述符的一些高级设计不会涉及到,但是我们能够搞懂它的原理即可,关于描述符的这些高级应用,下面的一篇文章会继续讲解,有兴趣的小伙伴们可以继续关注一下!