描述符 |
描述符也是面向进阶的一种,由于它的涉及比较广,所以单独讲。
一、描述符
描述符本质就是一个新式类,在这个新式类中,至少实现了__get__(),__set__(),__delete__()中的一个,这也被称为描述符协议。
描述符的作用是用来代理另外一个类的属性,必须把描述符定义成一个类的类属性,不能定义到构造函数中。
描述符分为两种,一种是数据描述符:至少实现了__get__()和__set__();另一种是非数据描述符:没有实现__set__()。
1.描述符格式
class Foo: #在python3中任何类都是新式类,它只要实现了三种方法之一,那么这个类就被称作一个描述符def __get__(self, instance, owner): #调用一个属性时触发passdef __set__(self, instance, value): #为一个属性赋值时触发passdef __delete__(self, instance): #采用del删除属性时触发pass
2.描述符的使用
描述符该如何使用呢?具体代码如下:
class Foo: #描述符def __get__(self, instance, owner):print('我是get方法')def __set__(self, instance, value):print('我是set方法')def __delete__(self, instance):print('我是delete方法') class Bar:x = Foo() #把描述符代理一个类的属性def __init__(self,n): self.x = nb1 = Bar(10) b1.x = 11111 b1.x del b1.x
执行代码结果为:
我是set方法
我是set方法
我是get方法
我是delete方法
可以看出在实例化时也会触发__set__方法,因为在类Bar的初始化函数中self.x是被描述符代理的属性。那么对象b1中的属性字典中到底存不存在x这个值呢?具体代码如下所示:
class Foo(): #描述符def __get__(self, instance, owner):print('我是get方法')def __set__(self, instance, value):print('我是set方法')def __delete__(self, instance):print('我是delete方法') class Bar:x = Foo()def __init__(self,n): self.x = nb1 = Bar(10) print(b1.__dict__) b1.x = 11111 print(b1.__dict__)
执行结果为:
我是set方法
{}
我是set方法
{}
这是什么情况?无论是实例化操作还是赋值操作,在对象b1的属性字典中仍然为空。正是因为描述符的关系,它相当于把被描述符的类的调用属性操作、赋值操作、删除操作都赋予另一个类来实现,跟本身类并没有关系,当然这关系到优先级的问题。
3.描述符的优先级
我们要严格遵守优先级:类属性>数据描述符>实例属性>非数据描述符>找不到的属性触发__getattr__()。
(1)我们先对类属性>数据描述符进行分析,具体代码如下:
class Foo():def __get__(self, instance, owner):print('我是get方法')def __set__(self, instance, value):print('我是set方法')def __delete__(self, instance):print('我是delete方法') class Bar:x = Foo()Bar.x=111 print(Bar.x) #结果为:111
上述代码打印结果仍为111,并没有执行数据描述符,是因为Bar类调用了本来要被描述符代理的属性x进行了修改。所有类属性的优先级比数据描述符高。
(2)数据描述符>实例属性的分析代码如下:
class Foo():def __get__(self, instance, owner):print('我是get方法')def __set__(self, instance, value):print('我是set方法')def __delete__(self, instance):print('我是delete方法') class Bar:x = Foo()b1 = Bar() b1.x #结果为:我是get方法 b1.x = 111 #结果为:我是set方法
上述代码Foo类被定义成Bar类的类属性,即对Bar类进行实例化,实例属性只执行数据描述符的方法。说明了数据描述符的优先级高于实例属性。
(3)实例属性>非数据描述符的分析代码如下:
class Foo():def __get__(self, instance, owner):print('我是get方法') class Bar:x = Foo()b1 = Bar() b1.x = 111 print(b1.__dict__) #结果为:{'x': 111}
上述代码可以看出实例给自己属性进行赋值操作,可以在实例的属性字典中找到,这说明了实例属性的优先级高于非数据描述符。
(4)非数据描述符>找不到的属性触发__getattr__()的分析代码如下:
class Foo():def __get__(self, instance, owner):print('我是get方法') class Bar:x = Foo()def __getattr__(self, item):print('我是getattr方法') b1 = Bar() b1.x #结果为:我是get方法 b1.name #结果为:我是getattr方法
上述代码可以看出找得到时触发__get__方法,找不到就会触发__getattr__方法。说明了非数据描述符的优先级高于找不到的属性触发__getattr__()。
4.描述符的应用
描述符可以应用到哪些场合呢?我们就来举个例子,通过描述符机制来实现参数的赋值类型限制。即:
class Typed:def __init__(self,key,expected_type):self.key = keyself.expected_type = expected_typedef __get__(self, instance, owner):print('get方法')return instance.__dict__[self.key]def __set__(self, instance, value):print('set方法')if not isinstance(value,self.expected_type):raise TypeError('你传入的类型不是%s' %self.expected_type)instance.__dict__[self.key] = valuedef __delete__(self, instance):print('delete方法')instance.__dict__.pop(self.key) class People:name = Typed('name',str)age = Typed('age',int)salary = Typed('salary',float)def __init__(self,name,age,salary):self.name = nameself.age = ageself.salary = salaryp1 = People('alex',18,6666.66) p2 = People(250,26,4568.55) #不符合赋值类型,抛出异常
上述代码对象p1是满足参数的赋值类型,所以会触发三次__set__方法。而对象p2不符合赋值类型,就会抛出异常。
5.propetry
一个静态属性property本质就是实现了__get__,__set__,__delete__三种方法。
propetry有两种用法,第一种即:
class Foo:@property #静态属性def AAA(self):print('get的时候运行')@AAA.setterdef AAA(self,value):print('set的时候运行',value)@AAA.deleterdef AAA(self):print('delete的时候运行') f1 = Foo() f1.AAA f1.AAA='aaa' del f1.AAA
执行结果为:
get的时候运行
set的时候运行 aaa
delete的时候运行
上述代码中只有在属性AAA定义property后才能定义AAA.setter,AAA.deleter。
第二种用法的代码实现如下:
class Foo:def get_AAA(self):print('get的时候运行')def set_AAA(self,value):print('set的时候运行',value)def del_AAA(self):print('delete的时候运行')AAA = property(get_AAA,set_AAA,del_AAA) #内置property三个参数与get,set,delete一一对应 f1 = Foo() f1.AAA f1.AAA='aaa' del f1.AAA
执行结果为:
get的时候运行
set的时候运行 aaa
delete的时候运行
上述两种用法的结果都是一样的。
我们也可以利用描述符自定制property,实现延时计算。我们先对一段代码进行分析,来看看我们利用描述符自定制property该如何实现相同的目的。即:
class Room:def __init__(self,name,width,length):self.name = nameself.width = widthself.length = length@property #area=property(area)def area(self):return self.width*self.length r1 = Room('别墅',15,16) print(r1.area) #结果为:240
在上述代码中@property相当于实现了area=property(area),而property它是一个类,这是不是相当于property类定义成Room类的类属性,这不就是描述符的性质吗?它既然是描述符,那么它是数据描述符还是非数据描述符呢?从结果它打印实例属性可以看出,它是非数据描述符,因为实例属性的优先级>非数据描述符。假如我们自定制property,不采用内置的静态属性property,该如何实现上述代码,即:
class Lazyproperty:def __init__(self,func):self.func = funcdef __get__(self, instance, owner):return self.func(instance) #instance实例本身 class Room:def __init__(self,name,width,length):self.name = nameself.width = widthself.length = length@Lazyproperty #area=Lazyproperty(area)def area(self):return self.width*self.length r1 = Room('别墅',15,16) print(r1.area) #结果为:240
上述代码中的@Lazyproperty是我自定制的,当然Lazyproperty类中我们只能定义成非数据描述符,否则不会执行area方法。
我们继续利用自定制property来实现延时计算功能:
class Lazyproperty:def __init__(self,func):self.func = funcdef __get__(self, instance, owner):print('get')if instance is None: #被类调用必须写,否则报错,因为类没有实例return selfres = self.func(instance) #instance实例本身setattr(instance,self.func.__name__,res) #缓存return res class Room:def __init__(self,name,width,length):self.name = nameself.width = widthself.length = length@Lazyproperty #area=Lazyproperty(area)def area(self):return self.width*self.length r1 = Room('别墅',15,16) print(r1.area) #从字典里先找,因为实例属性>非数据描述符,没有再去类的中找,然后出发了area的__get__方法 print(r1.area) #先从自己的属性字典找,找到了,是上次计算的结果,这样就不用每执行一次都去计算
上述代码实现了延时计算功能,这样这不会每次都打印get的操作。描述符是可以实现大部分python类特性中的底层魔法,包括@classmethod,@staticmethd,@property甚至是__slots__属性。
6.类的装饰器
类的装饰器与函数的装饰器性质是一样的,类的装饰器分为无参装饰器和有参装饰器。
我们先来定义一个无参的装饰器,即:
def deco(func):print('============')func.x = 1func.y = 2return func @deco #相当于Foo = deco(Foo) class Foo:pass print(Foo.__dict__)
执行结果为:
============ {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None, 'x': 1, 'y': 2}
上述代码当程序遇到@deco就马上执行了Foo=deco(Foo),可以在结果可以看出Foo类中的属性字典中有键值对x与y。
下面我们来介绍有参的装饰器该如何定义:
def Typed(**kwargs):def deco(obj):for key,val in kwargs.items():setattr(obj,key,val)return objreturn deco@Typed(x=1,y=2,z=3) #1.Typed(x=1,y=2,z=3)--->deco 2.@deco----->Foo=deco(Foo) class Foo:pass print(Foo.__dict__)@Typed(name='alex') #@deco---->Bar=deco(Bar) class Bar:pass print(Bar.name)
执行的结果为:
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None, 'x': 1, 'y': 2, 'z': 3} alex
上述代码定义了有参的装饰器,函数Typed是用来接收所有参数的。
下面我们利用装饰器的应用以及描述符来实现参数的赋值类型限制,即:
class Typed:def __init__(self,key,expected_type):self.key = keyself.expected_type = expected_typedef __get__(self, instance, owner):print('get方法')return instance.__dict__[self.key]def __set__(self, instance, value):print('set方法')if not isinstance(value,self.expected_type):raise TypeError('你传入的类型不是%s' %self.expected_type)instance.__dict__[self.key] = valuedef __delete__(self, instance):print('delete方法')instance.__dict__.pop(self.key)def deco(**kwargs):def wrapper(obj):for key,val in kwargs.items():setattr(obj,key,Typed(key,val)) #Typed(key,val)描述符return objreturn wrapper@deco(name = str,age = int,salary = float) class People:def __init__(self,name,age,salary):self.name = nameself.age = ageself.salary = salaryp1 = People('alex',20,6666.66)
实例化时触发了三次__set__方法。在描述符里也规定了参数的赋值类型。