一、核心要义
描述符是对多个属性运用相同存取逻辑的一种方式。其是实现了特定协议的类,常见协议包括__get__,__set__,__delete__方法。上一章介绍的property类就实现了完整的描述符协议。
二、代码示例
1、LineItem类第3版
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2024/3/17 12:46
# @Author : Maple
# @File : 01-LineItem类第3版.py
# @Software: PyCharm
"""一个简单的描述符类"""
class Quantity:
def __init__(self,storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be >0')
class LineItem:
weight = Quantity('weight')
price = Quantity('price')
def __init__(self,description,weight,price):
self.description = description
# 这里赋值的时候,会访问描述符类的set方法
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def __repr__(self):
return 'LineItem =({},{},{})'.format(self.description,self.weight,self.price)
if __name__ == '__main__':
# 1.传入正常的值
com = LineItem('Computer',10,2000)
print(com) # LineItem =(Computer,10,2000)
# 2.weight负值测试
try:
com3 = LineItem('Computer', -10, 1000)
except ValueError as e:
print(e) # value must be >0
2、LineItem类第4版
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2024/3/17 9:36
# @Author : Maple
# @File : 02-LineItem类第4版.py
# @Software: PyCharm
"""自动获取存储属性的值"""
class Quantity:
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix,index)
cls.__counter += 1
def __get__(self, instance, owner):
if instance is None:
# 通过托管类(此例中的LineItem)直接访问类属性时(此例中的weight和price时,instance为None,如果直接执行getattr,会报错
# 因此在此进行一下处理:直接返回描述符类
return self
else:
return getattr(instance,self.storage_name)
def __set__(self, instance, value):
if value > 0:
return setattr(instance,self.storage_name,value)
else:
raise ValueError('value must be >0')
class LineItem:
weight = Quantity()
price = Quantity()
def __init__(self,description,weight,price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
if __name__ == '__main__':
# 1.初始化
"""初始化流程分析
1.当执行到self.weight的时候,会去调用Quantity的__set__方法,然后给LineItem实例的'_Quantity#0'属性赋值100
2.当执行到self.price的时候,会去调用Quantity的__set__方法,然后给LineItem实例的'_Quantity#1'属性赋值100000
"""
computer = LineItem('Computer',100,100000)
# 1-1 注意观察实例属性,并非weight和price,而是_Quantity#0和_Quantity#1
print(computer.__dict__) # {'description': 'Computer', '_Quantity#0': 100, '_Quantity#1': 100000}
# 1-2 类属性中包含weight和price,并且它们都是Quantity描述符类对象
print(LineItem.__dict__) # {....'weight': <__main__.Quantity object at 0x000001594A20AB20>, 'price': <__main__.Quantity object at 0x000001594A1E0700>,...}
# 2.访问属性
## 流程分析:会去调用Quantity的__get__方法,然后返回computer中'_Quantity#0'属性的值
weight = computer.weight
print(weight) # 100
# 3.通过托管类直接访问类属性:返回描述符类本身
print(LineItem.weight) #<__main__.Quantity object at 0x00000266E579AB20>
3、LineItem类第5版
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2024/3/17 12:24
# @Author : Maple
# @File : 03-LineItem类第5版.py
# @Software: PyCharm
"""创建两个抽象类
1.AutoStorage:自动管理存储属性的描述符类
2.Validated:扩展AutoStorage类的抽象子类,覆盖__set__方法,调用必须由子类实现的validate方法
3.实现类继承Validated,从而可以创建具备不同规则的描述符类
"""
import abc
class AutoStorage:
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix,index)
cls.__counter += 1
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance,self.storage_name)
def __set__(self, instance, value):
setattr(instance,self.storage_name,value)
class Validated(abc.ABC,AutoStorage):
def __set__(self, instance, value):
value = self.validate(instance,value)
super().__set__(instance,value)
@abc.abstractmethod
def validate(self,instance,value):
"""retuen validated value or raise ValueError"""
class Quantity(Validated):
"""a number greater than zero"""
def validate(self,instance,value):
if value > 0:
return value
else:
raise ValueError('value must be >0')
class NonBlank(Validated):
"""a string with at least one non-space character"""
def validate(self,instance,value):
value = value.strip()
if len(value) == 0:
raise ValueError('value can not be empty or blank')
else:
return value
class LineItem:
description = NonBlank()
weight = Quantity()
price = Quantity()
def __init__(self,description,weight,price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def __repr__(self):
return 'LineItem =({},{},{})'.format(self.description,self.weight,self.price)
if __name__ == '__main__':
# 1.传入正常的值
com1 = LineItem('Computer',10,1000)
print(com1) # LineItem =(Computer,10,1000)
# 2.description传入空值
try:
com2 = LineItem('', 10, 1000)
except ValueError as e:
print(e) # value can not be empty or blank
# 3.weight负值测试
try:
com3 = LineItem('Computer', -10, 1000)
except ValueError as e:
print(e) # value must be >0
4、覆盖与非覆盖描述符
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2024/3/17 11:04
# @Author : Maple
# @File : 04-覆盖与非覆盖描述符.py
# @Software: PyCharm
# 将包含parent包的路径添加进系统路径
import sys
sys.path.append(r"D:\01-study\python\fluent_python\20-属性描述符\utils")
from descriptorkinds import print_args,display
class Overriding:
"""也称数据描述符或强制描述符"""
def __get__(self, instance, owner):
print_args('get',self,instance,owner)
def __set__(self, instance, value):
print_args('set',self,instance,value)
class OverridingNoGet:
"""没有get方法覆盖型描述符"""
def __set__(self, instance, value):
print_args('set',self,instance,value)
class NonOverrding:
"""也称非数据描述符或遮盖型描述符"""
def __get__(self, instance, owner):
print_args('get',self,instance,owner)
class Managed:
over = Overriding()
over_no_get = OverridingNoGet()
non_over = NonOverrding()
def spam(self):
print('--> Managed.spam({})'.format(display(self)))
if __name__ == '__main__':
print('*****1.覆盖型描述符测试****************')
# 1.覆盖型描述符测试
obj = Managed()
# 1-1.实例访问类属性(是一个描述符实例): 会触发描述符的get方法
obj.over # -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
# 1-2.类访问类属性(是一个描述符实例):同样会触发描述符的get方法,不过传入get方法的instance为None
Managed.over # -> Overriding.__get__(<Overriding object>, None, <class Managed>)
# 1-3.实例属性修改over,会触发描述符的set方法
obj.over = 7 # -> Overriding.__set__(<Overriding object>, <Managed object>, 7)
# 1-4.实例属性访问over,仍然会触发描述符的get方法
obj.over # -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
# 1-5.跳过描述符,直接通过dict赋值属性
obj.__dict__['over'] = 8
# 实例属性中确实有一个over实例属性(注意该同名的实例属性与类属性[对应的是描述符over]并不是同一个东西)
print(vars(obj)) # {'over': 8}
# 1-6 实例属性访问over,仍然会触发描述符的get方法,而不是直接访问实例属性
obj.over # -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
print('*****2.没有get方法的覆盖型描述符****************')
# 2.没有get方法的覆盖型描述符测试
# 2-1 因为over_no_get描述符并没有get方法,因此下面的操作是直接访问 Managed的类属性
print(obj.over_no_get) # <__main__.OverridingNoGet object at 0x000001CAA655A370>
print(Managed.over_no_get) # <__main__.OverridingNoGet object at 0x000001CAA655A370>
# 2-2.实例给属性赋值,会触发描述符的set方法
obj.over_no_get = 7 # -> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
# 2-3 由于上步操作并没有修改属性,因此以下访问的仍然是Manged的类属性
print(obj.over_no_get)
# 2-4 通过实例的__dict__属性设置名为over_no_get的实例属性
obj.__dict__['over_no_get'] = 7
# 由于实例属性会覆盖类属性,因此以下会直接读取实例属性的值
print(obj.over_no_get) # 7
# 但作为类属性的over_no_get 并没有发生变化
print(Managed.over_no_get) #<__main__.OverridingNoGet object at 0x000001F24D80A370>
# 2-5 仍然会触发描述符的set方法(但注意set方法并没有修改实例属性over_no_get的值)
obj.over_no_get = 9 #-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 9)
# 2-6 实例属性over_no_get的值并没有发生变化
print(obj.over_no_get) # 7
print('*****3.非覆盖型描述符****************')
# 3.非覆盖型描述符测试
## 3-1 实例访问non_over,会触发实例的get方法
obj.non_over
## 3-2 实例修改non_over,因为描述符没有set方法,所以相当于直接修改实例的实例属性
obj.non_over = 7
## ***然后实例属性会覆盖同名的描述符属性:这是一个规则吗?如何理解***
print(obj.non_over) # 7,不会走描述符的get逻辑
## 3-3 通过类访问non_over,描述符仍然存在
Managed.non_over # -> NonOverrding.__get__(<NonOverrding object>, None, <class Managed>)
## 3-4 删除实例属性non_over
del obj.non_over
## 3-5 再次通过实例访问non_over,此时会走描述符的get方法
obj.non_over # -> NonOverrding.__get__(<NonOverrding object>, <Managed object>, <class Managed>)
# 4.在类中覆盖描述符
print('*****4.在类中覆盖描述符****************')
# 直接通过类修改类属性值(修改之后描述符不存在,变成单纯的类属性,而实例属性的优先级会大于类属性),此时并不会走描述符的set方法[作为对比:通过类读取类属性,会走描述符的get方法]
# 这种现象也反映了 读写属性的一种不对等
Managed.over = 1
Managed.over_no_get = 2
Managed.non_over = 3
## 直接返回实例属性,不会经过描述符get逻辑--描述符已被销毁了
print(obj.over) #8
print(obj.over_no_get) #7
print(obj.non_over) #3
# 5.方法是描述符
## 5-1 通过obj.span获取的是一个`绑定方法对象`,可以观察到是method类型,而不是function
## 5-2 该方法的参数:__main__.Managed object at 0x00000186C6D7A400>,是一个Managed(托管类)实例
print(obj.spam) # <bound method Managed.spam of <__main__.Managed object at 0x00000186C6D7A400>>
## 5-3 通过类.spam获取的则是方法本身
print(Managed.spam) # <function Managed.spam at 0x00000291750E8790>
## 5-4 由于函数是非覆盖型(未实现set方法)的描述符,如果通过obj.spam赋值(相当于新增实例属性),会覆盖描述符
## 此时spam方法还可以调用
obj.spam() # --> Managed.spam(<Managed object>)
## 相当于给obj新增一个实例属性,spam描述符会被覆盖,因此方法也无法调用
obj.spam = 10
try:
obj.spam()
except Exception as e:
print(e) # 'int' object is not callable
## 访问spam实例属性的值
print(obj.spam) # 10