当前位置: 首页 > 科技观察

这个Python知识点,90%的人都要挂了~_0

时间:2023-03-21 12:36:56 科技观察

学了这么久的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"".format(self.name,self.math,self.chinese,self.english)看起来合理>>>std1=Student('小明',76,87,68)>>>std1<学生:小明,数学:76,语文:87,英语:68>但是程序没有人的智能,不会根据使用场景自动判断数据的有效性。如果老师在输入年级的时候,不小心输入了负数的分数,或者超过了100,程序就会感知不到。聪明的你,立马在代码中加入了判断逻辑。classStudent:def__init__(self,name,math,chinese,english):self.name=nameif0<=math<=100:self.math=mathelse:raiseValueError("Validvaluemustbein[0,100]")if0<=chinese<=100:self.chinese=chineseelse:raiseValueError("Validvaluemustbein[0,100]")if0<=chinese<=100:self.english=englishelse:raiseValueError("Validvaluemustbein[0,100]")def__repr__(self):return"".format(self.name,self.math,self.chinese,self.english)这个程序有点人工智能,能看得懂从错误本身。程序很聪明,但是__init__中的判断逻辑太多,大大影响了代码的可读性。巧的是,你刚刚了解了Property特性,在这里可以很好的应用。于是你修改代码如下,代码的可读性一下子提高了很多english=english@propertydefmath(self):returnself._math@math.setterdefmath(self,value):if0<=value<=100:self._math=valueelse:raiseValueError("Validvaluemustbein[0,100]")@propertydefchinese(self):返回自我。_chinese@chinese.setterdefchinese(self,value):if0<=value<=100:self._chinese=valueelse:raiseValueError("Validvaluemustbein[0,100]")@propertydefenglish(self):returnsself._english@english.setterdefenglish(self,value):if0<=value<=100:self._english=valueelse:raiseValueError("Validvaluemustbein[0,100]")def__repr__(self):return"".format(self.name,self.math,self.chinese,self.english)程序还是一样的人工智能,很不错。你觉得你写的代码已经很好,无可挑剔了。没想到有一天,你的主管看了你的代码,深深地叹了口气:班里的三个属性,math,Chinese,English,都用Property来检查属性的合法性。控制。从功能上来说,没有问题,就是太啰嗦了。三个变量的合法逻辑是一样的,只要大于0小于100即可。代码重复率太高了。这里的三个结果都不错,但是假设还是有地理、生物、历史、化学等几十个科目,这个代码简直受不了。让我们了解一下Python的描述符。经过主管的指导,你就知道“描述符”了。怀着敬畏之心,您搜索描述符的用法。其实也很简单。实现描述符协议的类是描述符。Whatdescriptorprotocol:类中至少实现了__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<=100:raiseValueError('Validvaluemustbein[0,100]')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=chineself.english=englishdef__repr__(self):return"".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__")returnsself._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("call__getattribute__")returnsuper(Student,self).__getattribute__(item)def__repr__(self):return"".,self.chinese)需要注意的是math是数据描述符,而chinese是非数据描述符。从下面的验证可以看出,当实例属性和数据描述符同名时,数据描述符(如下面的数学)会被优先访问,当实例属性和非数据描述符同名时同名,会先访问实例属性(__getattribute__)>>>std=Student('xm',88,99)>>>>>>std.math调用__getattribute__访问数据描述符中的__get__88>>>std.chinesecalls__getattribute__99Finished对于数据描述符和非数据描述符,我们还需要了解对象属性的查找规则。当我们访问一个实例属性时,Python会按照obj.__dict__→type(obj).__dict__→type(obj)的父类.__dict__的顺序进行查找。如果找到目标属性并且发现它是一个描述符,Python调用描述符协议来改变默认的控制行为。3、如何根据描述符实现属性经过上面的讲解,我们已经知道了如何定义描述符,也了解了描述符的工作原理。一般人见过的描述符的用法就是上面说的那些。我想说的是,它只是描述符协议最常见的应用之一。也许你还不知道。其实Python的特性有很多底层实现机制。它们都是基于描述符协议,比如我们熟悉的@property、@classmethod、@staticmethod和super。先说属性。有了前面的基础,我们知道了属性的基本用法。这里我直接进入正题,从第一篇文章的例子中提炼出来。classStudent:def__init__(self,name):self.name=name@propertydefmath(self):returnself._math@math.setterdefmath(self,value):if0<=value<=100:self._math=valueelse:raiseValueError("Validvaluemustbein[0,100]")我想简单回顾一下它的用法。带有属性修饰的函数,例如例子中的math,将成为Student实例的属性。为math属性赋值将进入使用math.setter装饰函数的逻辑代码块。为什么说property底层是基于描述符协议的呢?单击以通过PyCharm输入属性的源代码。遗憾的是,它只是一个类似于文档的伪源代码,没有其具体的实现逻辑。但是,从这段伪源码的魔法函数结构的组成,我们可以大致知道它的实现逻辑。这里我模仿它的函数结构,结合“描述符协议”,自己实现类属性的特性。代码如下: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:returnsselfifself.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):returnsself._math@math.setterdefmath(self,value):if0<=value<=100:self._math=valueelse:raiseValueError("Validvaluemustbein[0,100]")为了让大家不那么糊涂,这里有两个解释:用TestProperty修饰后,math是没有的不再是函数,而是TestProperty类的实例,因此第二个数学函数可以用math.setter修饰。其实质就是调用TestProperty.setter生成一个新的TestProperty实例,赋值给第二个math。第一个数学和第二个数学是两个不同的TestProperty实例。但它们都属于同一个描述符类(TestProperty)。给math赋值的时候,会进入TestProperty.__set__,求值的时候,会进入TestProperty.__get__。仔细一看,其实最终访问的还是Student实例的_math属性。说了这么多,我们来运行一下,这样更直观。#运行后直接打印这一行,是实例化TestProperty赋值给第二个mathinsetter>>>>>>s1.math=90in__set__>>>s1.mathin__get__90上面property的运行原理很难理解同学们请务必参考我上面写的两条说明。有其他问题可以加微信和我一起讨论。4、如何根据描述符实现staticmethod说完property,我们再来说说@classmethod和@staticmethod的实现原理。我这里定义了一个类,使用两种方式来实现静态方法。classTest:@staticmethoddefmyfunc():print("hello")#上下写法是等价的classTest:defmyfunc():print("hello")#重点:这是描述符的体现)写法是等价的,就像在属性中一样。其实下面两种写法也是等价的。@TestPropertydefmath(self):returnsself._mathmath=TestProperty(fget=math)让我们回到staticmethod。从上面的注释可以看出,staticmethod其实相当于一个描述符类,此时myfunc就变成了一个描述符。关于staticmethod的实现,大家可以参考我下面写的代码来理解。调用这个方法就知道,每次调用都会经过描述符类的__get__。>>>Test.myfunc()instaticmethod__get__hello>>>Test().myfunc()instaticmethod__get__hello五、如何根据descriptor实现classmethodclassmethod也是一样。classclassmethod(对象):def__init__(self,f):self.f=fdef__get__(self,instance,owner=None):print("inclassmethod__get__")defnewfunc(*args):returnself.f(owner,*args)returnnewfuncclassTest:defmyfunc(cls):print("hello")#重点:这是描述符的体现myfunc=classmethod(myfunc)验证结果如下说完property、staticmethod和classmethod与descriptor的关系。我想你应该对描述符在Python中的应用有了更深入的了解。至于super的实现原理,就靠你自己去完成了。6.所有实例共享描述符学习了以上内容,是不是觉得对描述符的了解已经够多了?但是在这里,我要说的是,上面的描述符代码是有问题的。哪里有问题?看看下面的例子。classScore:def__init__(self,default=0):self._value=defaultdef__get__(self,instance,owner):returnself._valuedef__set__(self,instance,value):if0<=value<=100:self._value=valueelse:raiseValueErrorclassStudent:math=Score(0)chinese=Score(0)english=Score(0)def__repr__(self):return"".format(self.math,self.chinese,self.english)Student中没有像以前那样写构造函数,但是重点不在这里,没必要就不写了。那我们看看会出现什么样的问题>>>std1=Student()>>>std1>>>std1.math=85>>>std1>>>std2=Student()>>>std2#std2其实共享std1>>>std2的属性值。math=100>>>std1#std2也会改变std1的属性值从结果看,std2其实共享std1的属性值,只要其中一个instance变量发生变化,另一个实例的变量也会发生变化。根本原因是此时math、Chinese、English都是类变量,所以std2和std1访问math、Chinese、English这三个变量时,实际上是在访问类变量。有问题吗?小明和小强的分数怎么能绑定呢?这显然与实际业务不符。描述符的使用,给我们带来了方便,但也在无形中给我们带来了麻烦。这也是描述符的一个特征吗?描述符是一个非常有用的特性。出现这个问题是因为我们之前写的描述符代码全错了。描述符的机制,在我看来只是抢占了访问顺序,但是具体的逻辑还得因地制宜,视情况而定。如果想把math、chinese、english这三个变量变成相互隔离的属性,应该这样写。classScore:def__init__(self,subject):self.name=subjectdef__get__(self,instance,owner):returninstance.__dict__[self.name]def__set__(self,instance,value):if0<=value<=100:instance.__dict__[self.name]=valueelse:raiseValueErrorclassStudent:math=Score("math")chinese=Score("chinese")english=Score("english")def__init__(self,math,chinese,english):self.math=mathself.chinese=chineseself.english=englishdef__repr__(self):return"".format(self.math,self.chinese,self.english)引导逻辑进入描述符之后,无论是获取属性还是设置属性,都是直接应用到实例上的。您可以仔细比较这段代码和前面的代码。不难看出:之前的错误代码更像是将描述符作为存储节点。之后正确的代码是直接使用描述符作为代理而不存储值本身。以上就是我对描述符的全部分享,希望对大家有所帮助。