# 第20章 属性描述符
"""
描述符: 是对多个属性运用相同存取逻辑的一种方式
描述符是实现了特定协议的类
包括__get__.__set__.__delete__
property实现了完整的描述符协议
使用描述符的python功能还有staticmethod\classmethod装饰器
理解描述符是精通python的关键
"""
# 20.1描述符示例:验证属性
# 20.1.1 LineItem类第三版:一个简单的描述符类
# 实现了__get__.__set__.__delete__的类是描述符,描述符的用法是创建一个实例,作为另一个类的类属性
"""
定义
描述符类:实现描述符协议的类,如下例中的Quantity
托管类:把描述符类的实例申明为类属性的类,如下例中的LineItem
描述符实例:描述符类的各个实例,申明为托管类的类属性
托管实例:托管类的实例,如下例中的LineItem类的实例
存储属性:托管实例中存储自身托管属性的属性,如下例中的LineItem类的实例的weight和price属性
托管属性:托管类中由描述符实例处理的公开属性
值存储在存储属性中,这种属性和描述符实例不同,描述符属性都是类属性
"""
# 示例20-1 bulkfood_v3.py:使用Quantity描述符管理LineItem类的属性
# 20.1.2 LineItem类第四版:自动获取存储属性的名称
# 为了避免重复输入属性的名称,为每一个Quantity实例的storage_name属性生成一个独一无二的字符串
# 为了生成storage_name 我们以"_Quantity#"为前缀,后面拼接一个整数
# Quantity.__counter类属性的当前值,每次把一个新的描述符实例依附到类上,都会递增这个值
# 示例 20-2 bulkfood_v4.py:每个Quantity描述符都有独一无二的storage_name
# 示例 20-3 bulkfood_v4b.py:通过托管类调用时,__get__方法返回描述符的引用
"""
通常,我们不会在使用描述符的模块中定义描述符类
而是在一个单独的模块中定义,以便在整个应用中使用
"""
# 示例 20-4 bulkfood_v4c.py :整洁的LineItem类,Quantity在model_v4c模块中
# 使用上一章的特性工厂函数实现和描述符类相同的功能
# 示例20-5 bulkfood_v4prop.py:使用特性工厂函数实现和描述符类相同的功能
# 描述符类可以使用子类扩展
# 20.1.3 LineItem类第5版:一种新型描述符
"""
在上述虚构的有机食物网店遇到一个问题:
有个商品的描述信息为空,导致无法下单
为解决这个问题,再创建一个描述符--NonBlank
它和Quantity描述符很像,只是验证逻辑不同
回想Quantity描述符
功能是管理托管实例的存储属性,以及验证用于设置那两个属性的值
重构这个描述符类,并创建两个基类
AutoStorage
自动管理存储属性的描述符类
Validated
扩展AutoStorage类的抽象子类,覆盖__set__方法,调用必须由子类实现的validate方法
"""
# 示例20-6 model_v5.py:重构构的描述符类
'''model_v5脚本的用户不需要知道全部细节
只需知道,可以实用Quantity和NonBlank自动验证属性的值'''
# 示例20-7 bulkfood_v5.py:使用Quantity和NonBlank描述符的LineItem类
# 20.2 覆盖型和非覆盖型描述符对比
"""
python存取属性的方式特别不对等:
通过实例读取属性时,通常返回实例中定义的属性
如果实例中没有指定的属性,那么会获取类属性
而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类属性
根据是否定义了__set__方法,描述符可以分为覆盖型和非覆盖型
"""
# 示例20-8 descriptorkinds.py:几个简单的类用于研究描述符的覆盖行为
# 20.2.1覆盖型描述符
"""
实现了__set__方法的描述符属于覆盖型描述符
因为虽然描述符是类属性,但是实现了__set__方法的话,会覆盖对实例属性的赋值操作"""
# 20.2.2没有__get__方法的覆盖型描述符
"""
覆盖型描述符通常会实现__set__和__get__方法,不过也可以只实现__set__方法
此时,只有写操作会由描述符处理,通过实例读取描述符会返回描述符本身
因为没有处理读值操作的__get__方法
如果直接通过实例的__dict__属性创建同名属性,以后再设置这个属性时,
仍会由__set__方法处理,但是读取这个属性,就会直接从属性中返回这个值,而不会返回描述符对象
"""
# 20.2.3非覆盖型描述符
# 20.2.4 在类中覆盖描述符
"""不管描述符是不是覆盖型
为类属性赋值都能覆盖描述符,这是一种猴子补丁技术"""
# 20.3方法是描述符
"""
在类中定义的函数属于绑定方法(bound method)
因为用户定义的函数都有__get__方法,所以依附到类上时,就相当于描述符
"""
# 示例20-14 method_is_descriptor.py:Text类,继承自UserString类
# 20.4 描述符与用法建议
"""结论:
1.使用特性以保持简单
内置的property类创建的其实是覆盖型描述符 __set__和__get__都实现了
2.只读描述符必须由__set__方法
否则实例的同名属性会掩盖描述符
3.用于验证的描述符可以只有__set__方法
4.仅有__get__方法的描述符可以实现高效缓存
5.非特殊的方法可以被实例属性覆盖
"""
"""
描述符的文档字符串用于注解托管类中各个描述符实例
"""
# 20.5 描述符的文档字符串和覆盖删除操作
示例20-1 bulkfood_v3.py:使用Quantity描述符管理LineItem类的属性
# 示例20-1 bulkfood_v3.py:使用Quantity描述符管理LineItem类的属性
class Quantity: # 1
def __init__(self,storage_name):
self.storage_name = storage_name # 2
def __set__(self, instance, value): # 3
if value > 0 :
# 错误示范
# self.__dict__[self.storage_name] = value 不能把值存储在描述符实例中
instance.__dict__[self.storage_name] = value # 4
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity('weight') # 5 这里需要重复输入weight,如果能weight = Quantity()就好了
price = Quantity('price') # 6
def __init__(self,description,weight,price): # 7
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
"""
解释:
1.描述符基于协议实现,无需创建子类
2.Quantity有一个storage_name属性,这是托管实例中存储值的属性的名称
3.尝试为托管属性赋值时会触发__set__方法,self是描述符实例,instance是托管实例,value是要设置的值
4.直接处理托管实例的__dict__属性,如果使用内置方法setattr()会触发无限递归
5.第一个描述符实例绑定给weight属性
6.第二个描述符实例绑定给price属性
7.余下的代码和上一章中的特性工厂函数相同
"""
if __name__ == '__main__':
truffle = LineItem('White truffle',100,0) # ValueError: value must be > 0
# 示例 20-2 bulkfood_v4.py:每个Quantity描述符都有独一无二的storage_name
# 示例 20-2 bulkfood_v4.py:每个Quantity描述符都有独一无二的storage_name
class Quantity:
__counter = 0 # 1
def __init__(self):
cls = self.__class__ # 2
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix,index) # 3
cls.__counter += 1 # 4
def __get__(self,instance,ower): # 5
# ower是托管类的引用
return getattr(instance,self.storage_name) # 6 因为名称不同,所以不会触发描述符,不会无限递归
def __set__(self, instance, value):
if value > 0 :
setattr(instance,self.storage_name,value) # 7
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity() #8
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
"""
说明:
1.__counter是Quantity类的类属性,用来统计Quantuty实例的数量
2.cls是Quantity类的引用
3.每一个描述符实例都是独一无二的,如_Quantity#0
4.递增__counter属性的值
5.要实现__get__方法,因为托管属性的名称和storage_name不同
6.使用内置的getattr方法从instance中获取存储属性的值
7.使用内置的setattr把值存储在instance中
8.不用把托管属性的名称传给Quantity构造方法,这是一版的目标
"""
if __name__ == '__main__':
thing = LineItem('thing',10,20)
print(thing.weight, thing.price)
print(getattr(thing, '_Quantity#0'))
print(getattr(thing, '_Quantity#1'))
thing.price = 100
print(thing.price)
print(LineItem.weight)
# 示例 20-3 bulkfood_v4b.py:通过托管类调用时,__get__方法返回描述符的引用
# 示例 20-3 bulkfood_v4b.py:通过托管类调用时,__get__方法返回描述符的引用
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,ower):
# ower是托管类的引用
if instance is None:
return self # 如果不是通过实例调用,返回描述符自身
else: # 否则像之前一样返回托管属性的值
return getattr(instance,self.storage_name)
def __set__(self, instance, value):
if value > 0 :
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__':
thing = LineItem('thing',10,20)
print(thing.weight, thing.price)
print(getattr(thing, '_Quantity#0'))
print(getattr(thing, '_Quantity#1'))
thing.price = 100
print(thing.price)
print(LineItem.weight)
# 示例 20-4 bulkfood_v4c.py :整洁的LineItem类,Quantity在model_v4c模块中
# 示例 20-4 bulkfood_v4c.py :整洁的LineItem类,Quantity在model_v4c模块中
import model_v4c as model
class LineItem:
weight = model.Quantity()
price = model.Quantity()
def __init__(self,description,weight,price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
# 示例20-5 bulkfood_v4prop.py:使用特性工厂函数实现和描述符类相同的功能
# 示例20-5 bulkfood_v4prop.py:使用特性工厂函数实现和描述符类相同的功能
def quantity():
try:
quantity.counter += 1 # 把counter定义为函数自身的属性
except AttributeError:
quantity.counter = 0 # 如果属性未定义,将其值设置为0
storage_name = '_{}:{}'.format('quantity',quantity.counter) # 将创建一个局部变量借助闭包保存它的值
def qty_getter(instance):
return getattr(instance,storage_name)
def qty_setter(instance,value):
if value > 0 :
setattr(instance,storage_name,value)
else:
raise ValueError('value must be >0')
return property(qty_getter,qty_setter)
class LineItem:
weight = quantity('weight')
price = quantity('price')
def __init__(self,description,weight,price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
# model_v4c.py 在单独的模块中定义描述符类
# model_v4c.py 在单独的模块中定义描述符类
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,ower):
# ower是托管类的引用
if instance is None:
return self # 如果不是通过实例调用,返回描述符自身
else: # 否则像之前一样返回托管属性的值
return getattr(instance,self.storage_name)
def __set__(self, instance, value):
if value > 0 :
setattr(instance,self.storage_name,value)
else:
raise ValueError('value must be > 0')
# 示例20-6 model_v5.py:重构构的描述符类
# 示例20-6 model_v5.py:重构构的描述符类
import abc
class AutoStorage: #1 AutoStorage提供了之前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,ower):
# ower是托管类的引用
if instance is None:
return self # 如果不是通过实例调用,返回描述符自身
else: # 否则像之前一样返回托管属性的值
return getattr(instance,self.storage_name)
def __set__(self, instance, value):
setattr(instance,self.storage_name,value) #2 ..验证部分除外
class Validated(abc.ABC,AutoStorage): # 3 抽象类也继承AutoStorage
def __set__(self, instance, value):
value = self.validate(instance,value) # 4 把验证操作委托给validate方法
super().__set__(instance,value) # 5 然后把返回的值传给超类的__set__方法
@abc.abstractmethod
def validate(selfs,instance,value): # 6 validate是抽象方法
'''return validated value or raise ValueError'''
class Quantity(Validated): # 7 Quantity和NonBlank都继承自Validated
'''a number greater than zero'''
def validate(selfs,instance,value):
if value <= 0:
raise ValueError('value must be > 0 ')
return value
class NonBlank(Validated):
'''a string with at least one non-space character'''
def validate(selfs,instance,value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value # 8 要求具体的方法返回验证后的值,借机可以清理\转换和规范化接收的数据
if __name__ == '__main__':
help(Quantity)
# 示例20-7 bulkfood_v5.py:使用Quantity和NonBlank描述符的LineItem类
# 示例20-7 bulkfood_v5.py:使用Quantity和NonBlank描述符的LineItem类
import model_v5 as model
class LineItem:
weight = model.Quantity()
price = model.Quantity()
description = model.NonBlank()
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__':
# stuff = LineItem('',10,0)
help(LineItem.weight)
# 示例20-8 descriptorkinds.py:几个简单的类用于研究描述符的覆盖行为
# 示例20-8 descriptorkinds.py:几个简单的类用于研究描述符的覆盖行为
def cls_name(obj_or_cls):
cls = type(obj_or_cls)
if cls is type:
cls = obj_or_cls
return cls.__name__.split('.')[-1]
def display(obj):
cls = type(obj)
if cls is type:
return '<class {}>'.format(obj.__name__)
elif cls in [type(None), int]:
return repr(obj)
else:
return '<{} object>'.format(cls_name(obj))
def print_args(name, *args):
pseudo_args = ', '.join(display(x) for x in args)
print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))
class Overriding: # 1 有__get__和__set__的典型覆盖型描述符
'''也称数据描述符或强制描述符'''
def __get__(self, instance, owner):
print_args('get', self, instance, owner) # 2 在这个示例中,各个描述符的每个方法都调用了print_args函数
def __set__(self, instance, value):
print_args('set', self, instance, value)
class OverridingNoGet: # 3
'''没有__get__方法的覆盖性描述符'''
def __set__(self, instance, value):
print_args('set', self, instance, value)
class NonOverriding: # 4 没有__set__
'''也称非数据描述符或非覆盖型描述符'''
def __get__(self, instance, owner):
print_args('get', self, instance, owner)
class Managed: #5 托管类,使用各个描述符的一个实例
over = Overriding()
over_no_get = OverridingNoGet()
non_over = NonOverriding()
def spam(self): # 6 spam方法放在这里是为了对比,因为方法也是描述符
print('-> Managed.spam({})'.format(display(self)))
if __name__ == '__main__':
# 覆盖型描述符的行为
"""
obj = Managed()
print(obj.over) # 触发__get__方法,第二个参数的值是托管实例obj
print(Managed.over) # 触发描述符的__get__方法,第二个参数(instance)的值是None
obj.over = 7 # 为obj.over赋值触发__set__方法,最后一个参数是7
print(obj.over) # 读取obj.over,扔会触发描述符的__get__方法
obj.__dict__['over'] = 8 # 跳过描述符,直接通过__dict__属性设置值
print(vars(obj)) # 确认值在obj.__dict__属性中,在over键下
print(obj.over) #然而,即使是名为over的实例属性,Managed.over描述符扔会覆盖读取obj.over这个操作
"""
# 没有__get__方法的覆盖性描述符
"""
obj = Managed()
print(obj.over_no_get)
print(Managed.over_no_get)
obj.over_no_get = 7
print(obj.over_no_get)
obj.__dict__['over_no_get'] = 9
print(obj.over_no_get)
obj.over_no_get = 7
print(obj.over_no_get)
"""
# 非覆盖型描述符
"""obj = Managed()
print(obj.non_over)
obj.non_over = 7
print(obj.non_over)
print(Managed.non_over)
del obj.non_over
print(obj.non_over)"""
# 通过类可以覆盖任何描述符
"""obj = Managed()
Managed.over = 1
Managed.over_no_get = 2
Managed.non_over = 3
print(obj.over, obj.over_no_get, obj.non_over) # 描述符不见了"""
# 方法是非覆盖型描述符
obj = Managed()
print(obj.spam) # <bound method Managed.spam of <__main__.Managed object at 0x00000245DD80BA90>>
print(Managed.spam) # <function Managed.spam at 0x00000217BD3B0670>
obj.spam = 7 # 如果为obj.spam赋值,会掩盖类属性,导致无法通过obj实例访问spam方法
print(obj.spam)
# 示例20-14 method_is_descriptor.py:Text类,继承自UserString类
# 示例20-14 method_is_descriptor.py:Text类,继承自UserString类
import collections
class Text(collections.UserString):
def __repr__(self):
return 'Text({!r})'.format(self.data)
def reverse(self):
return self[::-1]
word = Text('forward')
print(repr(word))
print(repr(word.reverse()))
print(repr(Text.reverse(Text('backword')))) # 在类上调用方法相当于调用函数
print(type(Text.reverse), type(word.reverse)) #类型是不同的,一个是function一个是method
# Text.reverse相当于函数,甚至可以处理Text之外的对象
print(list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')])))
# 函数都是非覆盖性描述符,在函数上调用__get__方法时,传入实例,得到的是绑定到那个实例上的方法
print(Text.reverse.__get__(word))
# 在函数上调用__get__方法时,传入实例是None,得到的是函数本身
print(Text.reverse.__get__(None, word))
print(word.reverse) # word.reverse其实会调用Text.reverse.__get__(word)返回对应的绑定方法
print(word.reverse.__self__)#word.reverse有个__self__属性,其值为调用这个方法的实例的引用
print(word.reverse.__func__ is Text.reverse) # 绑定方法的__func__属性时依附在托管类上那个原始函数的引用
35岁学python,也不知道为了啥?
本文详细介绍了Python中的描述符,包括描述符如何用于属性管理和验证,以及它们如何与实例属性交互。通过示例展示了描述符的实现,如Quantity和NonBlank,用于限制LineItem类的weight和price属性必须大于零,以及方法作为描述符的特性。文章还探讨了覆盖型和非覆盖型描述符的区别,并提供了使用特性工厂函数替代描述符的实现方式。
491

被折叠的 条评论
为什么被折叠?



