本文声明:python的描述符descriptor,这是属于python高级编程的一些概念和实现方法,可能有很多的小伙伴还并没有用到过,但是在Python的面试过程中有可能会出现,究竟什么是python描述符,有什么作用,使用有什么意义,它的诞生背景是什么,很少有文章专门介绍这一块,有的文章介绍的太过粗浅,以至于看过之后依然不能够理解描述符的本质。鉴于此,我寻思着出一期专门讲解python描述符的系列文章,跟前面的python装饰器系列文章一样,因为涉及到的内容偏多,本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第四篇——补充篇,介绍Python的描述符怎么实现一些python的高级功能,比如自定义实现@staticmethod、@property等功能。
上一篇文章中已经讲到,python描述符是可以实现大部分python类特性中的底层魔法,本文将以此为根基,实现几个基本的底层实现。
一、python描述符descriptor实现底层原理
1、实现底层@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灵活多变的地方,运行并不会出现错误。
2、实现底层 @staticmethod
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参数,否则会显示参数错误。
3、实现底层 @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__方法得到了进一步简化。
4、使用描述符实现属性类型约束
众所周知,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描述符的各种功能是非常强大的,这里就不一一实现了,有兴趣的小伙伴可以自己不断尝试哦!