留言有礼,每天都会从留言中选出三位抽取6.6元红包
学习Python这么久了,说起Python的优雅之处,能让我脱口而出的,Descriptor(描述符)特性可以排得上号。
描述符是Python语言独有的特性,它不仅在应用层使用,在语言语法糖的实现上也有使用到(在下面的文章会一一介绍)。
当你点进这篇文章时
你也许没学过描述符,甚至没听过描述符。或者你对描述符只是一知半解无论你是哪种,本篇都将带你全面的学习描述符,一起来感受Python语言的优雅。
1.为什么要使用描述符?假想你正在给学校写一个成绩管理系统,并没有太多编码经验的你,可能会这样子写。
classStudent:def__init__(self,name,math,chinese,english):self.name=nameself.math=mathself.chinese=chineseself.english=englishdef__repr__(self):return"Student:{},math:{},chinese:{},english:{}".format(self.name,self.math,self.chinese,self.english)
看起来一切都很合理
std1=Student(小明,76,87,68)std1Student:小明,math:76,chinese:87,english:68
但是程序并不像人那么智能,不会自动根据使用场景判断数据的合法性,如果老师在录入成绩的时候,不小心录入了将成绩录成了负数,或者超过,程序是无法感知的。
聪明的你,马上在代码中加入了判断逻辑。
classStudent:def__init__(self,name,math,chinese,english):self.name=nameif0=math=:self.math=mathelse:raiseValueError("Validvaluemustbein[0,]")if0=chinese=:self.chinese=chineseelse:raiseValueError("Validvaluemustbein[0,]")if0=chinese=:self.english=englishelse:raiseValueError("Validvaluemustbein[0,]")def__repr__(self):return"Student:{},math:{},chinese:{},english:{}".format(self.name,self.math,self.chinese,self.english)
这下程序稍微有点人工智能了,能够自己明辨是非了。
程序是智能了,但在__init__里有太多的判断逻辑,很影响代码的可读性。巧的是,你刚好学过Property特性,可以很好的应用在这里。于是你将代码修改成如下,代码的可读性瞬间提升了不少
classStudent:def__init__(self,name,math,chinese,english):self.name=nameself.math=mathself.chinese=chineseself.english=english
propertydefmath(self):returnself._mathmath.setterdefmath(self,value):if0=value=:self._math=valueelse:raiseValueError("Validvaluemustbein[0,]")propertydefchinese(self):returnself._chinesechinese.setterdefchinese(self,value):if0=value=:self._chinese=valueelse:raiseValueError("Validvaluemustbein[0,]")propertydefenglish(self):returnself._englishenglish.setterdefenglish(self,value):if0=value=:self._english=valueelse:raiseValueError("Validvaluemustbein[0,]")def__repr__(self):return"Student:{},math:{},chinese:{},english:{}".format(self.name,self.math,self.chinese,self.english)程序还是一样的人工智能,非常好。
你以为你写的代码,已经非常优秀,无懈可击了。
没想到,人外有天,你的主管看了你的代码后,深深地叹了口气:类里的三个属性,math、chinese、english,都使用了Property对属性的合法性进行了有效控制。功能上,没有问题,但就是太啰嗦了,三个变量的合法性逻辑都是一样的,只要大于0,小于就可以,代码重复率太高了,这里三个成绩还好,但假设还有地理、生物、历史、化学等十几门的成绩呢,这代码简直没法忍。去了解一下Python的描述符吧。
经过主管的指点,你知道了「描述符」这个东西。怀着一颗敬畏之心,你去搜索了下关于描述符的用法。
其实也很简单,一个实现了描述符协议的类就是一个描述符。
什么描述符协议:在类里实现了__get__()、__set__()、__delete__()其中至少一个方法。
__get__:用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常。__set__:将在属性分配操作中调用。不会返回任何内容。__delete__:控制删除操作。不会返回内容。对描述符有了大概的了解后,你开始重写上面的方法。
如前所述,Score类是一个描述符,当从Student的实例访问math、chinese、english这三个属性的时候,都会经过Score类里的三个特殊的方法。这里的Score避免了使用Property出现大量的代码无法复用的尴尬。
classScore:def__init__(self,default=0):self._score=defaultdef__set__(self,instance,value):ifnotisinstance(value,int):raiseTypeError(Scoremustbeinteger)ifnot0=value=:raiseValueError(Validvaluemustbein[0,])self._score=valuedef__get__(self,instance,owner):returnself._scoredef__delete__(self):delself._scoreclassStudent:math=Score(0)chinese=Score(0)english=Score(0)def__init__(self,name,math,chinese,english):self.name=nameself.math=mathself.chinese=chineseself.english=englishdef__repr__(self):return"Student:{},math:{},chinese:{},english:{}".format(self.name,self.math,self.chinese,self.english)
实现的效果和前面的一样,可以对数据的合法性进行有效控制(字段类型、数值区间等)
以上,我举了下具体的实例,从最原始的编码风格到Property,最后引出描述符。由浅入深,一步一步带你感受到描述符的优雅之处。
到这里,你需要记住的只有一点,就是描述符给我们带来的编码上的便利,它在实现保护属性不受修改、属性类型检查的基本功能,同时有大大提高代码的复用率。
2.描述符的访问规则描述符分两种:
数据描述符:实现了__get__和__set__两种方法的描述符非数据描述符:只实现了__get__一种方法的描述符你一定会问,他们有什么区别呢?网上的讲解,我看过几个,很多都把一个简单的东西讲得复杂了。
其实就一句话,数据描述器和非数据描述器的区别在于:它们相对于实例的字典的优先级不同。
如果实例字典中有与描述符同名的属性,如果描述符是数据描述符,优先使用数据描述符,如果是非数据描述符,优先使用字典中的属性。
这边还是以上节的成绩管理的例子来说明,方便你理解。
#数据描述符classDataDes:def__init__(self,default=0):self._score=defaultdef__set__(self,instance,value):self._score=valuedef__get__(self,instance,owner):print("访问数据描述符里的__get__")returnself._score#非数据描述符classNoDataDes:def__init__(self,default=0):self._score=defaultdef__get__(self,instance,owner):print("访问非数据描述符里的__get__")returnself._scoreclassStudent:math=DataDes(0)chinese=NoDataDes(0)def__init__(self,name,math,chinese):self.name=nameself.math=mathself.chinese=chinesedef__getattribute__(self,item):print("调用__getattribute__")returnsuper(Student,self).__getattribute__(item)def__repr__(self):return"Student:{},math:{},chinese:{},".format(self.name,self.math,self.chinese)
需要注意的是,math是数据描述符,而chinese是非数据描述符。从下面的验证中,可以看出,当实例属性和数据描述符同名时,会优先访问数据描述符(如下面的math),而当实例属性和非数据描述符同名时,会优先访问实例属性(__getattribute__)
std=Student(xm,88,99)std.math调用__getattribute__访问数据描述符里的__get__88std.chinese调用__getattribute__99
讲完了数据描述符和非数据描述符,我们还需要了解的对象属性的查找规律。
当我们对一个实例属性进行访问时,Python会按obj.__dict__→type(obj).__dict__→type(obj)的父类.__dict__顺序进行查找,如果查找到目标属性并发现是一个描述符,Python会调用描述符协议来改变默认的控制行为。
3.基于描述符如何实现property经过上面的讲解,我们已经知道如何定义描述符,且明白了描述符是如何工作的。
正常人所见过的描述符的用法就是上面提到的那些,我想说的是那只是描述符协议最常见的应用之一,或许你还不知道,其实有很多Python的特性的底层实现机制都是基于描述符协议的,比如我们熟悉的
property、classmethod、staticmethod和super等。先来说说property吧。
有了前面的基础,我们知道了property的基本用法。这里我直接切入主题,从第一篇的例子里精简了一下。
classStudent:def__init__(self,name):self.name=name
propertydefmath(self):returnself._mathmath.setterdefmath(self,value):if0=value=:self._math=valueelse:raiseValueError("Validvaluemustbein[0,]")不防再简单回顾一下它的用法,通过property装饰的函数,如例子中的math会变成Student实例的属性。而对math属性赋值会进入使用math.setter装饰函数的逻辑代码块。
为什么说property底层是基于描述符协议的呢?通过PyCharm点击进入property的源码,很可惜,只是一份类似文档一样的伪源码,并没有其具体的实现逻辑。
不过,从这份伪源码的魔法函数结构组成,可以大体知道其实现逻辑。
这里我自己通过模仿其函数结构,结合「描述符协议」来自己实现类property特性。
代码如下:
classTestProperty(object):def__init__(self,fget=None,fset=None,fdel=None,doc=None):self.fget=fgetself.fset=fsetself.fdel=fdelself.__doc__=docdef__get__(self,obj,objtype=None):print("in__get__")ifobjisNone:returnselfifself.fgetisNone:raiseAttributeErrorreturnself.fget(obj)def__set__(self,obj,value):print("in__set__")ifself.fsetisNone:raiseAttributeErrorself.fset(obj,value)def__delete__(self,obj):print("in__delete__")ifself.fdelisNone:raiseAttributeErrorself.fdel(obj)defgetter(self,fget):print("ingetter")returntype(self)(fget,self.fset,self.fdel,self.__doc__)defsetter(self,fset):print("insetter")returntype(self)(self.fget,fset,self.fdel,self.__doc__)defdeleter(self,fdel):print("indeleter")returntype(self)(self.fget,self.fset,fdel,self.__doc__)
然后Student类,我们也相应改成如下
classStudent:def__init__(self,name):self.name=name#其实只有这里改变
TestPropertydefmath(self):returnself._mathmath.setterdefmath(self,value):if0=value=:self._math=valueelse:raiseValueError("Validvaluemustbein[0,]")为了尽量让你少产生一点疑惑,我这里做两点说明:
使用TestProperty装饰后,math不再是一个函数,而是TestProperty类的一个实例。所以第二个math函数可以使用math.setter来装饰,本质是调用TestProperty.setter来产生一个新的TestProperty实例赋值给第二个math。
第一个math和第二个math是两个不同TestProperty实例。但他们都属于同一个描述符类(TestProperty),当对math对于赋值时,就会进入TestProperty.__set__,当对math进行取值里,就会进入TestProperty.__get__。仔细一看,其实最终访问的还是Student实例的_math属性。
说了这么多,还是运行一下,更加直观一点。
#运行后,会直接打印这一行,这是在实例化TestProperty并赋值给第二个mathinsetters1.math=90in__set__s1.mathin__get__90
对于以上理解property的运行原理有困难的同学,请务必参照我上面写的两点说明。如有
最近更新
推荐文章