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

Python黑魔法Descriptors

时间:2023-03-11 22:59:42 科技观察

简介描述符(descriptors)是Python语言中一个深奥但非常重要的黑魔法。在Python语言的内核中被广泛使用。工具箱增加了一个额外的技巧。在这篇文章中,我将描述描述符的定义和一些常见的场景,并在文章的最后添加__getattr、__getattribute__和__getitem__这三个同样涉及属性访问的魔术方法。描述符定义descr__get__(self,obj,objtype=None)-->valuedescr.__set__(self,obj,value)-->Nonedescr.__delete__(self,obj)-->None只需要一个对象属性(objectattribute)Any定义了上述三个方法之一,那么这个类就可以称为描述符类。描述符基础在下面的示例中,我们创建了一个RevealAcess类并实现了__get__方法。现在这个类可以称为描述符类。classRevealAccess(object):def__get__(self,obj,objtype):print('SelfinRevealAccess:{}'.format(self))print('self:{}\nobj:{}\nobjtype:{}'.format(self,obj,objtype))classMyClass(object):x=RevealAccess()deftest(self):print('selfinMyClass:{}'.format(self))EX1实例属性接下来我们来看看在__get__方法的每个参数的含义。在下面的示例中,self是RevealAccess类的实例x,obj是MyClass类的实例m,而objtype,顾名思义,就是MyClass类本身。从输出语句可以看出,m.x访问描述符x调用了__get__方法。>>>m=MyClass()>>>m.test()selfinMyClass:<__main__.MyClassobjectat0x7f19d4e42160>>>>m.xselfinRevealAccess:<__main__.RevealAccessobjectat0x7f19d4e420f0>self:<__main__.RevealAccessobjectat0x7f19d4e420f0>obj:<__main__.MyClassobjectat0x7f19d4e42160>objtype:EX2类属性如果直接通过类访问属性x,那么obj直接为None,相对容易理解,因为不存在MyClass的实例。>>>MyClass.xselfinRevealAccess:<__main__.RevealAccessobjectat0x7f53651070f0>self:<__main__.RevealAccessobjectat0x7f53651070f0>obj:Noneobjtype:描述符原理上面例子中的描述符触发,我们列举分别从实例属性和类属性的角度介绍描述符的用法。我们仔细分析一下内部原理:如果访问实例属性,其实是调用了基类对象的__getattribute__方法。在这个方法中,obj.d被翻译成type(obj).__dict__['d'].__get__(obj,type(obj))。如果是访问类属性,相当于调用元类类型的__getattribute__方法,将cls.d翻译成cls.__dict__['d'].__get__(None,cls),其中__get__()的obj是无,因为不存在实例。先简单说一下__getattribute__这个魔术方法。当我们访问一个对象的属性时,这个方法将被无条件地调用。__getattr和__getitem__的区别等详细内容我会在文末额外补充。我暂时不深究。描述符优先级首先,描述符有两种类型:如果一个对象同时定义了__get__()和__set__()方法,则该描述符称为数据描述符。如果对象只定义了__get__()方法,则该描述符称为非数据描述符。当我们访问属性时,有以下四种情况:数据描述符instancedict非数据描述符__getattr__()它们的优先级是:数据描述符>instancedict>非数据描述符>__getattr__()这是什么意思呢??也就是说,如果实例对象obj中存在同名的数据描述符->d和实例属性->d,当obj.d访问属性d时,由于数据描述符的优先级更高,Python会调用type(obj).__dict__['d'].__get__(obj,type(obj))而不是调用obj.__dict__['d']。但如果描述符是非数据描述符,Python将调用obj.__dict__['d']。Property每使用一个描述符就定义一个描述符类,看起来很繁琐。Python提供了一种向属性添加数据描述符的简洁方法。property(fget=None,fset=None,fdel=None,doc=None)->property属性fget,fset和fdel分别是类的getter、setter和deleter方法。我们用下面的例子来说明如何使用Property:classAccount(object):def__init__(self):self._acct_num=Nonedefget_acct_num(self):returnself._acct_numdefset_acct_num(self,value):self._acct_num=valuedefdel_acct_num(self):delself._acct_numacct_num=property(get_acct_num,set_acct_num,del_acct_num,'_acct_numproperty.')如果acct是Account的实例,acct.acct_num会调用getter,acct.acct_num=valuewill调用setter,delacct_num.acct_num会调用deleter。>>>acct=Account()>>>acct.acct_num=1000>>>acct.acct_num1000Python还提供了@property装饰器,可以用来为简单的应用场景创建属性。属性对象有getter、setter和deleter装饰器方法,可用于通过相应装饰函数的访问器函数创建属性的副本。classAccount(object):def__init__(self):self._acct_num=None@property#_acct_num属性。装饰器创建一个只读属性defacct_num(self):returnself._acct_num@acct_num.setter#_acct_num属性设置器使属性可写defset_acct_num(self,value):self._acct_num=value@acct_num.deleterdefdel_acct_num(self):delself._acct_num如果想让属性只读,只需要去掉setter方法。在运行时创建描述符我们可以在运行时添加属性:classPerson(object):defaddProperty(self,attribute):#createlocalsetterandgetterwithaparticularattributenamegetter=lambdaself:self._getProperty(attribute))setter=lambdaself,value:self._setProperty(attribute,value)#构造property属性并将其添加到类中setattr(self.__class__,attribute,property(fget=getter,\fset=setter,\doc="Auto-生成方法"))def_setProperty(self,attribute,value):print("Setting:{}={}".format(attribute,value))setattr(self,'_'+attribute,value.title())def_getProperty(self,attribute):print("Getting:{}".format(attribute))returngetattr(self,'_'+attribute)>>>user=Person()>>>user.addProperty('姓名')>>>你ser.addProperty('phone')>>>user.name='johnsmith'Setting:name=johnsmith>>>user.phone='12345'Setting:phone=12345>>>user.nameGetting:name'JohnSmith'>>>user.__dict__{'_phone':'12345','_name':'JohnSmith'}静态方法和类方法我们可以使用描述符来模拟Python中@staticmethod和@classmethod的实现。我们先看看下表:TransformationCalledfromanObjectCalledfromaClassfunctionf(obj,*args)f(*args)staticmethodf(*args)f(*args)classmethodf(type(obj),*args)f(klass,*args)静态方法f的静态方法。c.f和C.f是等价的,都是直接查询object.__getattribute__(c,'f')或object.__getattribute__(C,'f')。静态方法的一个明显特征是没有自变量。静态方法有什么用?假设有一个专门处理数据的容器类,它提供了一些计算平均值、中位数等统计数据的方法,而这些方法都依赖于相应的数据。但是,类中可能有一些方法不依赖于这些数据。这时候我们可以将这些方法声明为静态方法,这样也可以提高代码的可读性。使用非数据描述符模拟静态方法的实现:classStaticMethod(object):def__init__(self,f):self.f=fdef__get__(self,obj,objtype=None):returnself.fLet'sApplyit:classMyClass(object):@StaticMethoddefget_x(x):returnxprint(MyClass.get_x(100))#output:100classmethodsPython的@classmethod和@staticmethod的用法有些相似,但还是有些不同,当某些方法只需要获取类的引用而不关心类中对应的数据时,就需要使用classmethod。使用非数据描述符模拟类方法的实现:classClassMethod(object):def__init__(self,f):self.f=fdef__get__(self,obj,klass=None):ifklassisNone:klass=type(obj)defnewfunc(*args):returnself.f(klass,*args)returnnewfunc其他魔术方法***在接触Python魔术方法的时候,我也被__get__,__getattribute__,__getattr__坑了,__getitem__都是属性访问相关的魔术方法。重写__getattr__和__getitem__以构建您自己的集合类是很常见的。让我们通过一些例子来看看它们的应用。__getattr__Python对类/实例属性的默认访问是通过__getattribute__调用的。__getattribute__将被无条件调用。如果未找到,将调用__getattr__。如果我们要自定义某个类,通常我们不应该重写__getattribute__,而应该重写__getattr__,很少会看到重写__getattribute__的情况。从下面的输出可以看出,当无法通过__getattribute__找到属性时,将调用__getattr__。在[1]中:classTest(object):...:def__getattribute__(self,item):...:print('call__getattribute__')...:returnsuper(Test,self).__getattribute__(item)...:def__getattr__(self,item):...:return'call__getattr__'...:In[2]:Test().acall__getattribute__Out[2]:'call__getattr__'适用于默认字典,Python只支持obj['foo']形式的访问,不支持obj.foo形式。我们可以通过重写__getattr__让字典也支持obj['foo']的访问形式,这是一个非常经典和常用的用法:classStorage(dict):"""Storage对象就像一个字典,除了`obj.foo`可以在`obj['foo']`之外使用。"""def__getattr__(self,key):try:returnself[key]exceptKeyErrorask:raiseAttributeError(k)def__setattr__(self,键,值):self[key]=valuedef__delattr__(self,key):try:delself[key]exceptKeyErrorask:raiseAttributeError(k)def__repr__(self):return''让我们使用我们的自定义增强字典:>>>s=Storage(a=1)>>>s['a']1>>>s.a1>>>s.a=2>>>s['a']2>>>dels.a>>>s.a...AttributeError:'a'__getitem__getitem是使用下标[]的形式来获取其中的元素对象,让我们通过重写__getitem__来实现我们自己的列表类MyList(object):def__init__(self,*args):self.numbers=argsdef__getitem__(self,item):returnself.numbers[item]my_list=MyList(1,2,3,4,6,5,3)printmy_list[2]这个实现很简单,不支持slice和step等功能。请读者自行完善。这里我就不再赘述。应用下面参考requests库中__getitem__的使用。我们自定义了一个忽略属性大小写的字典类。程序有点复杂,稍微解释一下:由于比较简单,不需要使用描述符,所以使用了@property装饰器。lower_keys的作用是将实例字典中的所有key都转为小写,存入字典self._lower_keys中。重写了__getitem__方法。以后我们访问一个属性的时候,会先把key转成小写,然后就不会直接访问实例字典了,而是访问字典self._lower_keys来找。在赋值/删除操作过程中,实例字典会发生变化。为了保持self._lower_keys和实例字典的同步,需要先清除self._lower_keys中的内容,再次查找key时调用__getitem__重新创建一个新的self。._lower_keys。classCaseInsensitiveDict(dict):@propertydeflower_keys(self):如果不是hasattr(self,'_lower_keys')或者不是self._lower_keys:self._lower_keys=dict((k.lower(),k)forkinself.keys())returnself._lower_keysdef_clear_lower_keys(self):ifhasattr(self,'_lower_keys'):self._lower_keys.clear()def__contains__(self,key):返回key.lower()inself.lower_keysdef__getitem__(self,key):ifkeyinself:返回dict.__getitem__(self,self.lower_keys[key.lower()])def__setitem__(self,key,value):dict.__setitem__(self,key,value)self._clear_lower_keys()def__delitem__(self,key):dict.__delitem__(self,key)self._lower_keys.clear()defget(self,key,default=None):ifkeyinself:返回self[key]else:returndefault我们来调一下这个类:>>>d=CaseInsensitiveDict()>>>d['ziwenxie']='ziwenxie'>>>d['ZiWenXie']='ZiWenXie'>>>print(d){'ZiWenXie':'ziwenxie','ziwenxie':'ziwenxie'}>>>print(d['ziwenxie'])ziwenxie#d['ZiWenXie']=>d['ziwenxie']>>>print(d['ZiWenXie'])ziwenxie