kafka马士兵Java系统架构师

Python高级编程描述符Descriptor详解

2019-04-16  本文已影响0人  程序员1

全文摘要

本文声明:python的描述符descriptor,这是属于python高级编程的一些概念和实现方法,可能有很多的小伙伴还并没有用到过,但是在Python的面试过程中有可能会出现,究竟什么是python描述符,有什么作用,使用有什么意义,它的诞生背景是什么,很少有文章专门介绍这一块,有的文章介绍的太过粗浅,以至于看过之后依然不能够理解描述符的本质。上一篇文章中(中篇)已经讲到了关于属性的三中控制方式,讲解了三个魔术方法__getattr__、__setattr__、__delattr__的详细使用方法,也分析了它们的作用和不足之处。本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第三篇——下篇,介绍Python的描述符、描述符协议、描述符三剑客、描述符的详细实现等。

全文目录

01 到底什么是描述符——descriptor

1.1 什么是描述符——descriptor以及

相关的一系列定义

1.2 描述符的作用

1.3 描述符三个函数的定义形式

02 描述符的具体实现

2.1 认识描述符

2.2 类属性描述符

2.3 实例属性描述符

2.4 类属性描述符对象和实例属性同名时

03 属性优先访问级别总结

3.1 如果没有设置“描述符属性”

3.2 如果设置了“描述符属性”

3.3 疑惑不解

04 描述符的应用场景

05 描述符的底层应用(下期预告)

01

到底什么是描述符——descriptor

前面饶了很多弯子,一步一步引入属性访问的优先级顺序这样一个主题,然后是属性控制的三剑客,似乎还是和描述符descriptor没啥关系啊,关系自然是有的。本文会一一说明,首先我将一系列的概念和定义一次性的写出来,后面再加以分析说明。

01 什么是描述符

1.1 descriptor以及一系列定义

(1)描述符:某个类,只要是内部定义了方法 __get__, __set__, __delete__ 中的一个或多个,就可以称为描述符,描述符的本质是一个类。

(2)描述符协议:描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一个,这些魔术方法也被称为描述符协议

(3)非数据描述符:一个类,如果只定义了 __get__() 或者是__delete__()方法,而没有定义 __set__()方法,则认为是非数据描述符(即没有定义__set__)

(4)数据描述符:一个类,不仅定义了 __get__() 方法,还定义 __set__(), __delete__() 方法,则认为是数据描述符(即定义了__get__和__set__)

(5)描述符对象:描述符(即一个类,因为描述符的本质是类)的一个对象,一般是作为其他类对象的属性而存在

01 什么是描述符

1.2 描述符的作用

描述符就是一个“绑定行为“的对象属性,在描述符协议中,它可以通过方法充写属性的访问。我们或许经常看见描述符的作用描述中,有两个关键词“绑定行为”和“托管属性”,那到底是什么意思呢,我给出一个通俗的解释,

绑定行为:所谓的绑定行为,是指在属性的访问、赋值、删除时还绑定发生了其他的事情,正如前面属性控制三剑客所完成的事情一样;

托管属性:python描述符是一种创建“托管属性”的方法,即通过描述符(类)去托管另一个类的相关属性,也可以说是类的属性的一个代理。为了方便的理解“托管属性”这个概念,将通过几个通俗的例子去说明。

以人类而言,Person是一个类,人应该有很多属性,比如人是美丽的、性感的、成熟的、博学的、大方的等等,所谓的“描述”,本身指的就是描述某一个类的某一些特性的,在程序设计中,属性就是用来描述类的特征的,所谓的描述符(描述类)就是专门再创建一个类,让这个类去描述本身那个类的相关属性,这也正是“描述”二字的由来,其实和我们生活中的描述是一个意思。

描述符的作用是用来代理另外一个类的属性的

后面的代码也将从“绑定行为”和“托管属性”两个方面进行说明。

01 什么是描述符

1.3 描述符三个函数的定义形式

def __get__(self, instance, owner)

self:指的是描述符类的实例

instance:指的是使用描述符的那个类的实例,如

student。下面的instance一样的意思。

owner:指的是使用描述符的那个类,如Student

def __set__(self, instance, value)

def __delete__(self, instance,)

02

描述符的具体实现

前面讲了,要实现所谓的描述符,就是要实现上面的三个魔术方法,但是和普通类定义的方式不一样,因为“属性代理(属性托管)”的机制,我们需要定一两个类,一个类A,一个ADescriptor类,即所谓的描述类。

注意,不是直接在一个类中定义上面的描述符的三个方法哦!

02 描述符的具体实现

2.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)来访问。

02 描述符的具体实现

2.1 类属性描述符

依然用上面的代码,只是下面添加以下几句话。

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__方法;

02 描述符的具体实现

2.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的一个对象。

总结

描述符是一个类属性,必须定义在类的层次上, 而不能单纯的定义为对象属性。

通过上面的这几个例子,现在应该可以好好体会到“描述符”的两个层面的作用了:

绑定行为:在访问雷属性的时候,会打印出很多的额外信息,这不就是在添加额外的行为吗?

属性代理(托管属性):将某个属性专门用一个描述符(描述类)加以托管,实现任意的定制化,一对一的定制属性。

02 描述符的具体实现

2.4 类属性描述符对象和实例属性同名时

前面说了,描述符针对的是类属性,但是当一个类中,如果类属性是描述符对象,而实例属性由于这个描述符属性同名,这该怎么办呢?

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,但是最终打印出来值却并不是类属性的值,这是因为,实例属性实际上是在“描述符类属性”后面访问的,所以覆盖掉了。

总结

到目前为止,我们接触到的属性有很多了,实例属性,类属性、描述符类属性、父类的类属性、带有属性控制函数三剑客的属性等,那么当一个属性同名的时候,访问的优先级到底是什么样子呢?

03

属性的优先访问级别总结

03 属性优先访问级别总结

3.1 如果没有设置“描述符属性”

没有设置描述符属性,则属性的优先访问顺序和我们前面文章里面所讲的是一样的,

(1) __getattribute__(), 无条件调用,任何时候都先调用

(2)实例属性

(3)类属性

(4)父类属性

(5) __getattr__() 方法 #如果所有的属性都没有搜索到,则才会调用该函数

03 属性优先访问级别总结

3.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作为数据描述符存在。

03 属性优先访问级别总结

3.3 疑惑不解

我搜集了很多博文,看到很多博主得到了如下结论,导致我自己也没有得出一个确切的定论,所以希望再次与广大网友讨论,下面的两个结论都是从博客上摘录下来的。

(1)类属性 > 数据描述符 > 实例属性 > 非数据描述符 > 找不到的属性触发__getattr__()

这样的说法显然不严谨,因为类属性不总是优先于实例属性的

(2) __getattribute__()> 数据描述符> 实例对象的字典(若与描述符对象同名,会被覆盖哦)>类的字典>非数据描述符

>父类的字典>__getattr__() 方法

这样的说法也不严谨,因为从我上面的调试来看,当数据描述符属性与实力属性同名的时候,最终显示的值是实例属性的值,但是并不是实例属性覆盖了描述符属性,恰好相反,此时,实例属性也是当做描述属性那样去用的,而且调用了__get__和__set__方法。

总结

个人认为“描述符”的作用有其特殊性,它的目的并不是改变属性访问的优先级,根本目的只是改变属性的控制方式,方便对属性进行更好的访问、修改和删除,所以没必要死记硬背一个固定的优先级,在具体的问题中根据代码的运行能够做出合理的判断即可。当然如果哪一位小伙伴有更加权威的排序,也可以私下里告诉我哦,解答我心中的疑惑,将万分感谢!

04

“描述符”的应用场景

描述符的本质在于“描述”二字,最大的用处是对属性的个性定制与控制,如前所说,

(1)可以在设置属性时,做些检测等方面的处理

(2)设置属性不能被删除?那定义_delete_方法,并raise 异常。

(3)还可以设置只读属性

(4)把一个描述符作为某个对象的属性。这个属性要更改,比如增加判断,或变得更复杂的时候,所有的处理只要在描述符中操作就行了。

这一系列其实都是为了更好地去控制一个属性。

但是描述符因为它非常灵活的语法,可以实现一些非常高级的python特性,描述符是可以实现大部分python类特性中的底层魔法,含@classmethod,@staticmethd,@property甚至是__slots__属性,不仅如此,描述父是很多高级库和框架的重要工具之一,描述符通常是使用到装饰器或者元类的大型框架中的一个组件。

作为python使用者,可能绝大部分使用者对于描述符的一些高级设计不会涉及到,但是我们能够搞懂它的原理即可,关于描述符的这些高级应用,下面的一篇文章会继续讲解,有兴趣的小伙伴们可以继续关注一下!

Python高级编程——描述符Descriptor超详细讲解(补充篇之底层原理实现)

送你小心心记得关注我哦!!

进入正文

全文摘要

本文声明:前面的系列文章已经讲解了python属性的访问优先级、属性拦截器、Python的属性控制三剑客、Python的描述符协议,做了这么多的铺垫和原理讲解,但是还没有真正讲解python描述符的高级应用。看过前面文章的小伙伴一定有印象,Python描述符和Python装饰器息息相关,可以用来实现很多Python底层的相关设计,那具体到底是怎么实现的呢?这就是本文要解决的重点。本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第四篇——补充篇,介绍Python的描述符怎么实现一些python的高级功能,比如自定义实现@staticmethod、@classmethod、@property等功能。

上一篇文章中已经讲到,python描述符是可以实现大部分python类特性中的底层魔法,本文将以此为根基,实现几个基本的底层实现。

全文目录

python描述符descriptor实现底层原理

01 实现底层@classmethod

02 实现底层@stasticmethod

03 实现底层@property

04 使用描述符实现属性类型约束

01

实现底层@classmethod

众所周知,在python语言的面向对象中,使用@classmethod修饰的方法是类方法,类方法可以通过实例名称访问,也可以通过类名称访问,我们平时在使用的过程中,都是这么实用的,从来没有想过这在语言底层到底怎么实现呢?

先看一个简单的例子:

class Person:

def __init__(self):

pass

def study(cls):

print('我会搞学习!')

print(Person.study())

运行结果错误,显示TypeError: study() missing 1 required positional argument: 'cls'

我们都知道错误的原因是什么,我要定义类方法,我需要给study方法使用@classmethod装饰器,现在我自定义一个装饰器,我不使用原本的@classmethod,依然让它达到相同的效果。代码如下:

class NewDefine_classmethod:

"""

使用“描述符”和“装饰器”结合起来,模拟@classmethod

"""

def __init__(self, function):

self.function = function

def __get__(self, instance, owner):

#对传进函数进行加工,最后返回该函数

def wrapper(*args, **kwargs): #使用不定参数是为了匹配需要修饰的函数参数

print("给函数添加额外功能")

self.function(owner, *args, **kwargs)

return wrapper

class Person:

name='我有姓名'

def __init__(self):

pass

@NewDefine_classmethod

def study_1(cls):

print(f'我的名字是:{cls.name},我会搞学习!')

@NewDefine_classmethod

def study_2(cls,score):

print(f'我的名字是:{cls.name},我会搞学习!,而且这次考试考了 {score} 分')

print(Person.study_1())

print(Person.study_2(99))

运行结果为:

给函数添加额外功能

我的名字是:我有姓名,我会搞学习!

None

给函数添加额外功能

我的名字是:我有姓名,我会搞学习!,而且这次考试考了 99 分

None

那到底是怎么运行的呢?结合前面的装饰其原理,我们可以分这样几步分析:

第一步:@NewDefine_classmethod本质上是一个“类装饰器”,从它的定义可知,它的定义为

class NewDefine_classmethod(function).我们发现,python系统定义的@classmethod其实它的定义也是一样的,如下,class classmethod(function) .怎么样?它们二者的定义是不是一样?

第二步:NewDefine_classmethod本质上又是一个描述符,因为在它的内部实现了__get__协议,由此可见,NewDefine_classmethod是“集装饰器-描述符”于一身的。

第三步:运行过程分析,因为study_1=NewDefine_classmethod(study_1),所以,study_1本质上是一个NewDefine_classmethod的对象,又因为NewDefine_classmethod本质上是实现了描述符的,所以,study_1本质上是一个定义在类中的描述符属性。

第四步:因为study_1本质上是一个定义在类中的描述符属性。所以在执行Person.study_1的时候,相当于是访问类的描述符属性,所以会进入到描述符的__get__方法。

现在是不是觉得原来python描述符还有这样神奇的使用呢?

注意:如果修饰的函数本身是具有返回值的,在__get__里面所定义的wrapper里面一定要返回,即return self.function(owner, *args, **kwargs)。

还有一个地方需要注意的是,因为这是自定义的底层实现,所以一些集成IDE可能会显示有语法错误,但是这没有关系,这正是python灵活多变的地方,运行并不会出现错误。

02

实现底层@stasticmethod

staticmethod方法与classmethod方法的区别在于classmethod方法在使用需要传进一个类的引用作为参数。而staticmethod则不用。它们所遵循的原理是大致一样的,参见代码如下:

class NewDefine_staticmethod:

"""

使用“描述符”和“装饰器”结合起来,模拟@classmethod

"""

def __init__(self, function):

self.function = function

def __get__(self, instance, owner):

#对传进函数进行加工,最后返回该函数

def wrapper(*args, **kwargs): #使用不定参数是为了匹配需要修饰的函数参数

print("给函数添加额外功能")

self.function(*args, **kwargs)

return wrapper

class Person:

name='我有姓名'

def __init__(self):

pass

@NewDefine_staticmethod

def study_1(math,english):

print(f'我数学考了 {math} 分,英语考了 {english} 分,我会搞学习!')

@NewDefine_staticmethod

def study_2(history,science):

print(f'我历史考了 {history} 分,科学考了 {science} 分,我会搞学习!')

print(Person.study_1(99,98))

print(Person.study_2(88,89))

运行结果为:

给函数添加额外功能

我数学考了 99 分,英语考了 98 分,我会搞学习!

None

给函数添加额外功能

我历史考了 88 分,科学考了 89 分,我会搞学习!

None

整个函数的执行原理与上面所实现的是一样的,但是有一些小的细节需要注意。

类方法classmethod必须第一个参数是cls,这个实际上就是判断所属的那个类,因此在__get__里面的function在调用的时候,第一个参数需要传递为owner,因为所属的“类cls等价于Person等价于owner”,但是因为静态方法不需要任何参数cls或者是self都不需要,因此在__get__实现的时候不能再传递owner参数,否则会显示参数错误。

03

实现底层@property

在使用描述符实现property的时候和前面稍有所区别,后面会进行分析,代码如下:

class NewDefine_property:

"""

使用“描述符”和“装饰器”结合起来,模拟@classmethod

"""

def __init__(self, function):

self.function = function

def __get__(self, instance, owner):

print("给函数添加额外功能")

return self.function(instance)

class Person:

name='我有姓名'

def __init__(self):

self.__study=100

@NewDefine_property

def study_1(self): #使用property装饰的函数一般不要用“参数”,因为它的主要功能是对属性的封装

return self.__study

p=Person()

print(p.study_1)

运行结果为:

给函数添加额外功能

100

基本思想和前面分析的还是一样的,但是有几个地方有所区别,需要注意:

第一:@property的目的是封装一个方法,是这个方法可以被当做属性访问

第二:调用的方式与前面有所不同,__get__里面不能再定义wrapper了,否则不会调用wrapper。得不到想要的结果,为什么呢?

因为调用的方式不一样,根据前面的分析,study_1的本质是描述符属性,但是前面的调用均是使用的

Person.study_1()或者是p.study_1()的形式,还是当成方法去使用的。但是此处不一样了,直接就是当成属性去使用,

p.study_1 ,不再是方法调用,因此wrapper函数得不到调用。所以__get__方法得到了进一步简化。

04

使用描述符实现属性类型约束

众所周知,python是一门动态语言,属性的赋值可以是任意的,不像是静态语言那样,而我们前面讲过,描述符最核心的功能其实就是“属性代理”,即所谓的对属性进行加工改造,进行相关的约束。所以,如果想要对某一个属性的类型进行某种约束,描述符便可以很好地完成。实现的代码如下:

class Constraint_Property(object):

def __init__(self,var_name,var_type,var_default_value=None):

'''

var_name:变量名称

var_type:变量所要约束的类型,比如int、str、float等等

var_default_value:变量的初始默认值

'''

self.name=var_name

self.type=var_type

self.default=var_type() if var_default_value is None else var_default_value

#三元运算,如果使用了默认值就使用默认值,否则就是用某个类型的默认值,如int()、str()、float()

def __get__(self,instance,owner):

if self.default==None:

return self.type()

else:

return self.default

def __set__(self,instance,value):

if not isinstance(value,self.type):

raise TypeError("属性的值必须是: %s 类型",self.type)

self.default=value

def __delete__(self,instance):

raise AttributeError("不能删除该属性")

class Student(object):

name=Constraint_Property("name",str,"张三") #str虽然是类名,也是可以作为参数的,因为一切皆对象

age=Constraint_Property("age",int) #int虽然是类名,也是可以作为参数的,因为一切皆对象

stu=Student()

print(stu.name)

print(stu.age)

stu.name="李四"

print(stu.name)

stu.age=25

print(stu.age)

print('===================================')

stu.name=100.0 #赋一个实数值

运行结果为:

张三

0

李四

25

===================================

Traceback (most recent call last):TypeError: ('属性的值必须是: %s 类型', <class 'str'>)

从上面的实例可以看出,那么属性被限定为str,age的属性被限定为int。

小伙伴们,看了上面的这几个例子,是不是被python描述符的强大功能所折服呢?事实上python描述符的各种功能是非常强大的,这里就不一一实现了,有兴趣的小伙伴可以自己不断尝试!

下面给大家分享学习编程所需要学习的知识和PDF电子书,希望大家能够喜欢:

关注转发文章私信老师(学习)就可以了!

关注转发文章添加微信:15803464551

备注:A   就可获取到了!

麻烦转发一下文章!

上一篇下一篇

猜你喜欢

热点阅读