描述符的使用面很广,不过其主要的目的在于让我们的调用过程变得可控制。因此我们在一些需要对我们调用过程实行精细控制的时候,使用描述符。
描述符本质就是一个新式类,在这个新式类中,至少具备了get()
、set()
、delete()
其中一种,这也被称为:描述符协议
get():调用一个属性时触发
set():为一个属性赋值时触发
delete():使用del删除一个属性时触发
class An:
def __get__(self, instance, owner):
pass
def __set__(self, instance, value):
pass
def __delete__(self, instance):
pass
这种定义的类,已经具备了我们所列出的3个方法其中之一了,此时它可以称为:描述符
我们可以尝试来调用它
res = An()
res.name = 'jack'
print(res.name)
> 'jack'
可以看到,里面的set()
和get()
都没有触发,说明并不是这样使用描述符的。
而描述符为分为:数据描述符、非数据描述符
1、数据描述符:数据描述符定义了set 或 delete() 其中一个方法。而通常数据描述会具备:set() 与 get() 两个方法
2、非数据描述符:只具备set()方法
官方文档的解释
描述符的作用:它是在引用一个对象属性时自定义要完成的工作,可以很好规定我们定义属性的数据类型,避免代码的重复率
使用原则:用来代理另外一个类的属性的,必须把描述符定义成类的属性,且不能定义到构造函数中。
在此之前,先来看一下属性的查找顺序
class People:
name = '20'
def __init__(self,name,age):
self.name = name
sel.name = name
self.age = age
p = People('jack',18)
print(p.name)
> 'jack'
未产生意外,属性先在自身获取 -> 类的属性 -> … ,为什么要提这个,因为在使用描述符后,属性的查找顺序将发生改变!
描述符本身应该定义成新式类,被代理的类也应该是新式类(Python3中都是新式类)
必须把描述符定义成这个类的类属性,不能为定义到构造函数中
要严格遵循该优先级,优先级由高到底分别是
1、类属性
2、数据描述符
3、实例属性
4、非数据描述符
5、找不到的属性触发getattr()
这就是为何上面提到了属性的查找顺序,在我们使用描述符以后,查找将会被这个优先级所代替。
我们通过实例来了解:
# 描述符类
class An:
def __get__(self, instance, owner):
print('get触发了')
def __set__(self, instance, value):
print(f'set触发了')
def __delete__(self, instance):
print('delete触发了')
# 被描述的类
class People:
# 类属性name代理了An()这个描述符类
name = An()
def __init__(self,n,age):
# 重点!!!!
# self.name 此时,这里的self.name已经不是给对象的属性了
# 而是调用了类属性name,也就是上面那个
self.name = 123 # 这里表示调用类属性name代理的描述符里的set()方法并传了123这个值进去
# 简单来说就是,我们通过self找到的类属性里面的name,而并不是给对象赋值
self.nnm = n # 这里的nnm未被代理,所以可以赋给对象作为属性
self.age = age
p = People('jack',18)
和上面对比,我们的查找顺序已经发生了改变,直接类的属性了
直接运行:由于我们在类里面就调用了代理的描述符,所以触发了set方法
我们通过调用,类代理的描述符属性都会触发描述符里面的方法
p = People('jack',18)
p.name
这个name并不是对象的,而是类的,且对象也无法创建name这个属性了,因为在类里面name已经被代理,无论对name赋值,还是查询,都是指向到name代理的描述符里面去
执行效果
'get触发了'
删除代理描述符的类属性时
del p.name
执行效果
'delete触发了'
已经发现,优先级的查找顺序在代理描述符的那刻起,就发生了改变。只要类的属性有,那么就用类的属性。而类的属性对应的则是描述符。所以赋值还是查询等操作都是传到了描述符类的方法里面去了
上面只是简单属性描述符的使用,下面来详细了解
在调用描述符时,会传递一系列值进去。我们需要知道传递的值都是些什么。及什么情况下会触发
class An:
def __get__(self, instance, owner):
print(f'get触发了')
# 这个描述符自身的对象,就像我们之前定义的类里面,每个方法都会有一个self
print(f'描述符类的对象:{self} ')
# 也就是p对象
print(f'调用代理描述符的对象:{instance}')
# 实例化p对象的类,也就是People
print(f'调用代理描述符的对象所属的类:{owner}')
print(instance.__dict__)
def __set__(self, instance, value):
print(f'set触发了')
print(f'描述符类的对象:{self} ')
print(f'调用代理描述符的对象:{instance}')
print(f'调用代理描述符所传递的值:{value}')
print(instance.__dict__)
# 打印就可以知道,是不是调用代理描述符的对象了
def __delete__(self, instance):
print(f'delete触发了')
print(f'调用代理描述符的对象:{instance}')
print(instance.__dict__)
通过调用来查看结果:
class People:
name = An()
def __init__(self,name,age):
self.age = age
p = People('jack',18)
p.name = '123' # 调用描述符的代理(name),并进行传值,执行了描述符的set()
执行结果
'''
set触发了
描述符类的对象:<__main__.An object at 0x7fe60a286760>
代理描述符的对象:<__main__.People object at 0x7fe60a56ffd0>
调用描述符所传递的值:123
'''
{
'age': 18}
可以看到,调用代理描述符时,instance参数接收的接收调用代理描述符的对象,也就是我们这里的p对象,因为是它调用的
调用描述符
p.name # 执行描述符的get()
'''
get触发了
描述符类的对象:<__main__.An object at 0x7fdcb667e760>
代理描述符的对象:<__main__.People object at 0x7fdcb706ffd0>
代理描述符的对象所属的类:
'''
{
'age': 18}
删除描述符代理
del p.name # 执行描述符的delete()方法
'''
delete触发了
代理描述符的对象:<__main__.People object at 0x7facc116ffd0>
'''
{
'age': 18}
类来调用,就可以看到代理描述符的对象为None,基本不会这样调用
People.name # 执行描述符的get()
'''
get触发了
描述符类的对象:<__main__.An object at 0x7f9c0a186760>
调用代理描述符的对象:None
调用代理描述符的对象所属的类:
'''
AttributeError: 'NoneType' object has no attribute '__dict__'
发生报错,因为instance接收到的不是对象,所以值为None,打印__dict__对象属性时就产生了报错,了解即可,基本不会这样调用描述符。
我们再使用类来调用数据描述符,会发现一些奇怪的事情
People.name = '2'
执行效果
# 什么也没有发生,这种表示People修改了name属性,那么此时name属性就不再是一个代理
调用查看
print(People.name)
> '2'
可以发现,描述符只能作为类属性使用,即不适合被类使用,也不能放入构造函数__init__内,两者其一都会失去描述符的意义。所以我们通过实例化的对象来调用才是最适合的用法
向描述符传值操作与我们创建对象时的操作是一致的,但是不同点在于描述符类需要被调用才能生效。
class An:
def __init__(self, sex): # 接收到被描述类的代理属性传递的属性名
self.sex = sex # 此时这个sex代表'sex'是我们类属性传递过来的
def __get__(self, instance, owner):
print(f'get触发了')
return instance.__dict__[self.sex]
# 查询时返回查询的属性
def __set__(self, instance, value):
print(f'set触发了')
instance.__dict__[self.sex] = value
# 将接收到的值放入调用这个描述符的对象属性内(也就是p)
def __delete__(self, instance):
print(f'delete触发了')
class People:
# 通过An这个描述符实例化出一个对象,但暂时并不使用,只是起到一个传值操作
sex = An('sex')
def __init__(self, age):
self.age = age
p = People(18)
p.sex = 'male' # 调用描述符的set()方法并传值'male'到value参数
print(p.__dict__)
print(p.sex)
执行结果
'set触发了'
{
'age': 18, 'sex': 'male'}
'get触发了'
'male'
上序提到过,如果我们通过类名去调用会因为没有传递对象进去而报错,那么我们这里再来处理一下
People.sex
> AttributeError: 'NoneType' object has no attribute '__dict__'
我们只需要在查询描述符代理执行的get()方法时,做一个判断,那么就可以避免掉这个问题
def __get__(self, instance, owner):
print(f'get触发了')
if instance is None:
return '当前未通过对象调用描述符的代理'
return instance.__dict__[self.sex]
print(People.sex)
执行结果
'get触发了'
'当前未通过对象调用描述符的代理'
可以限制我们对属性的传递为何种类型,如果不对则给出提示信息,既然涉及到了传递,那么就要对set()方法做点手脚
class An: # 两个方法进行调整
def __init__(self, attri,data_type): # 接收到被描述类的代理属性传递的属性名
# 接收传递进来的第一个参数,我们将它作为属性名使用
self.attri = attri
# 接收传递进来的第而个参数,我们将它数据类型,待会与传到set里面的值做匹配
self.data_type = data_type
def __set__(self, instance, value):
# 判断我们通过=赋给代理属性的值是否匹配我们传给这个描述符这个数据类型
if not isinstance(value,self.data_type):
# 如果不支持,抛出类型错误的信息
raise TypeError(f'{self.attri}属性传递的不是:{self.data_type}类型数据')
instance.__dict__[self.arrti] = value
# 将属性添加到对象里面,这对象指定是调用代理描述符的那个类属性,也就是p
class People:
# 将参数传给了描述符里__init__
name = An('name',str)
# 这个name接收到的是000,因为是在调用类是传递进来的
def __init__(self,name, age):
# 把000赋给name = An('name',str)这个描述符代理
# 执行了描述符里面的set()方法,然后将000传给value参数
self.name = name
self.age = age # 未被代理,所以可以实例化给对象
p = People(000,18)
print(p.name)
因为传递的数据类型不匹配,所以抛出异常
我们将传递给描述符set()
的的值进行纠正
p = People('jack',18)
print(p.name)
执行结果
'get触发了'
'jack'
传递多个属性,代码稍作改动
# 被描述符类
class People:
# 第一个参数作为给p对象的属性名
name = An('name',str)
age = An('age', int)
sex = An('sex',str)
# 这些参数作为给p对象的属性值
def __init__(self,name, age,sex):
# 将它们赋给不同的描述符代理
self.name = name
self.age = age
self.sex = sex
p = People('jack',18,'sex')
print(p.name)
print(p.age)
print(p.sex)
执行结果
'''
get触发了
jack
get触发了
18
get触发了
sex
'''
到这里,我们已经可以实现我们的目的了,但是这样随着后期的属性增加,将会出现一堆的描述符代理,low,继续优化!
先来了解一下类的装饰器,哈哈,没听错,是给类使用的装饰器!它可以帮助我们优化描述符代理叠加的问题
先来了解无参的,有简入难
def decorate(cls):
print('类的装饰器开始运行------>')
cls.name = 'jack' # 给People类加上name属性
return cls # 返回了People这个类
@decorate
class People: # People = decorate(People)
pass
p = People() # 接收到People这个类,加上()调用类实例化了对象p
print(p.name) # p这个对象自身没有name这个属性,去People类里面找到
执行结果
'jack'
def arrti_datatype(**kwargs):
def decorate(cls):
for attri,data_type in kwargs.items():
setattr(cls,attri,data_type)
print('有参装饰器!-> %s' % kwargs)
return cls
return decorate
@arrti_datatype(name = str,age = int,sex = str)
class People:
def __init__(self,name,age,sex):
self.name = name
self.age = age
self.sex = sex
print(People.__dict__)
p = People('jack',18,'20')
执行效果
有参装饰器!-> {
'name': <class 'str'>, 'age': <class 'int'>, 'sex': <class 'str'>}
省略...<class 'str'>, 'age': <class 'int'>, 'sex': <class 'str'>}
可以看到,我们已经将这个我们制定好的参数变成我们的类属性了,只是目前这些类属性没有代理描述符!我们只需要类属性变成代理描述符的类属性即可大功告成!
不使用重复代码完成,限制属性设置的数据类型
class An:
def __init__(self,attri,data_type):
self.attri = attri
self.data_type = data_type
def __get__(self, instance, owner):
print(f'get触发了')
if instance is None:
return '当前未通过对象调用描述符的代理'
return instance.__dict__[self.attri]
def __set__(self, instance, value):
if not isinstance(value, self.data_type):
raise TypeError(f'{self.attri}属性传递的不是:{self.data_type}类型数据')
instance.__dict__[self.attri] = value
def __delete__(self, instance):
print('执行了delete方法:')
def arrti_datatype(**kwargs):
def decorate(cls):
for attri,data_type in kwargs.items():
# 注意这里,将传给类的属性变成代理描述符的类属性
# cls:People这个类
# attri:给People类设置的属性名
# An(attri,data_type) 将属性名与数据类型传递到描述符里的__init__内
# 得到一个代理描述符的对象,然后赋给cls,也就是People类
setattr(cls,attri,An(attri,data_type))
# setattr就是给对象增加属性的,类也可以是一个对象,也可以进行属性增加
return cls
return decorate
@arrti_datatype(name = str,age = int,sex = str)
class People:
def __init__(self,n,a,s):
# 此时name、age、sex都被描述符所代理,所以我们的赋值操作都是传到了描述符内的set方法里
self.name = n
self.age = a
self.sex = s
print(People.__dict__)
我们可以看到,这些类属性已经变成了代理描述符的类属性
省略... 'name': <__main__.An object at 0x7fbc1536ffd0>, 'age': <__main__.An object at 0x7fbc153b28b0>, 'sex': <__main__.An object at 0x7fbc153b2c40>}
我们查看执行效果,先故意传错一个属性,看看会抛出异常
p = People('jack',18,222)
p = People('jack',18,'male')
print(p.name)
print(p.age)
print(p.sex)
执行效果
'''
get触发了
jack
get触发了
18
get触发了
male
'''
我们的描述符类基本没有改变,关键点在于装饰器那里,给类属性赋值时,调用描述符,获得一个代理描述符的对象,然后赋给类作为属性。
到这里的相信已经会使用描述符进行操作了,根据自身需求制定描述符来对程序进行精确控制。请自行发挥!!
@property
装饰器是我们在学习封装时所用到的,它的作用是将方法伪装成一个属性,来回顾一下
class People:
@property
def test(self):
print('test')
p = People()
p.test
执行结果
'test'
我们也可以通过描述符来实现,自己写一个效果类似的
class customized:
def __init__(self, func):
self.func = func # 将bmi方法拿到
def __get__(self, instance, owner):
print('执行了定制pro方法')
if instance is None:
return '请使用对象调用'
# instance=就是调用bmi方法的对象,也就是p
return self.func(instance) # 将它传入进bmi方法内,它就可以使用self了
# 调用bmi方法,相当于 bmi(p) 拿到返回值,返回给调用者
class People:
def __init__(self, width, height):
self.width = width
self.height = height
@customized
def bmi(self): # bmi = customized(bmi),但是并没有把self传递进去
return self.width / (self.height ** 2)
p = People(70, 1.90)
print(round(p.bmi, 2)) # 通过round方法,将返回值保留小数点后两位
执行结果
'执行了定制pro方法'
'19.39'
效果就是,我们调用过一次bmi方法后,第二次不会再去调用,而是拿着第一次调用的结果直接打印,拿上边举例优化
p = People(70, 1.90)
print(round(p.bmi, 2)) # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2)) # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2)) # 通过round方法,将返回值保留小数点后两位
执行效果
'''
执行了定制pro方法
19.39
执行了定制pro方法
19.39
执行了定制pro方法
19.39
'''
可以看到,我们每次调用它的bmi
方法,所以我们让它第一次拿到结果以后,不再去运行bmi
方法计算获取结果
修改装饰器内的代码即可
class customized:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
print('执行了定制pro方法')
if instance is None:
return '请使用对象调用'
# 将方法名(bmi)作为属性名添加到对象内,再将计算的值添加进去
# 这样下次对象调用就能在自身获取到,不会再调用相同的方法来获取了
instance.__dict__[self.func.__name__] = self.func(instance)
return self.func(instance)
我们再来执行查看效果
print(round(p.bmi, 2)) # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2)) # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2)) # 通过round方法,将返回值保留小数点后两位
print(p.__dict__)
执行结果
'''
执行了定制pro方法
19.39
19.39
19.39
'''
{
'width': 70, 'height': 1.9, 'bmi': 19.390581717451525}
这样一来,我们第一次输入bmi是去调动bmi这个方法
,调用以后在装饰器内,将结果作为属性保存到了我们对象内,所以下一次调用则是去自身属性内找到了,所以直接打印。
技术小白记录学习过程,有错误或不解的地方欢迎在评论区留言,如果这篇文章对你有所帮助请
点赞、评论、收藏+关注
子夜期待您的关注,谢谢支持!