python高级编程——描述符Descriptor详解(下篇)——python描述符三剑客详解

本文声明:python的描述符descriptor,这是属于python高级编程的一些概念和实现方法,可能有很多的小伙伴还并没有用到过,但是在Python的面试过程中有可能会出现,究竟什么是python描述符,有什么作用,使用有什么意义,它的诞生背景是什么,很少有文章专门介绍这一块,有的文章介绍的太过粗浅,以至于看过之后依然不能够理解描述符的本质。鉴于此,我寻思着出一期专门讲解python描述符的系列文章,跟前面的python装饰器系列文章一样,因为涉及到的内容偏多,本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第三篇——下篇,介绍Python的描述符、描述符协议、描述符三剑客、描述符的详细实现等。

一、到底什么是描述符——descriptor

前面饶了很多弯子,一步一步引入属性访问的优先级顺序这样一个主题,然后是属性控制的三剑客,似乎还是和描述符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使用者,可能绝大部分使用者对于描述符的一些高级设计不会涉及到,但是我们能够搞懂它的原理即可,关于描述符的这些高级应用,下面的一篇文章会继续讲解,有兴趣的小伙伴们可以继续关注一下!

你可能感兴趣的:(python,设计模式,白话python高级特性,python高级编程,Python的__get__,__det__方法,python的托管属性,属性代理,python的属性访问)