属性描述符
描述符是对多个属性运用相同存取逻辑的一种方式。
描述符是实现了特定协议的类,包括__set__
、__get__
和__delete__
方法。
描述符验证属性
特性函数工厂函数借助函数式编程模式避免重复编写读值方法和设值方法
描述符用法:创建一个实例,作为另一个类的类属性。
- 使用Quantity描述符管理LineItem的属性
class Quantity:
"""描述符类,实现描述符协议的类"""
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value): # self是LineItem.weight描述符实例,instance是LineItem托管实例
if value > 0:
instance.__dict__[self.storage_name] = value # 直接处理托管实例的___dict__属性,避免无限递归
else:
raise ValueError('value must be > 0')
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
truffle = LineItem('White fruffle', 100, 0) # 小于0,抛出异常
- 自动获取存取属性的名称
class Quantity:
__counter = 0 # 类属性,统计实例数量
def __init__(self):
cls = self.__class__ # 类的引用
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index) # 生成独一无二的storage_name
cls.__counter += 1
def __get__(self, instance, owner): # owner是LineItem的引用
if instance is None: # 不是通过实例调用
return self
else:
return getattr(instance, self.storage_name) # 从instance中获取存储属性的值
def __set__(self, instance, value): # self是LineItem.weight,instance是LineItem实例
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
cocounts = LineItem('Brazilian cocount', 20, 17.95)
cocounts.weight, cocounts.price
getattr(cocounts, '_Quantity#0'), getattr(cocounts, '_Quantity#1')
LineItem.price
br_nuts = Linetem('Brazil nuts', 10, 34.95)
br_nuts.price
- 使用子类管理属性
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):
"""raise"""
class Quantity(Validated):
"""处理数值"""
def validate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
else:
return value
class NonBlank(Validated):
"""处理description为空"""
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value
class LineItem:
description = NonBlank()
weight = Quantity()
price = Quantity()
def __init__(self, description, weight, price):
self.description = description
self.price = price
self.weight = weight
def subtotal(self):
return self.weight * self.price
覆盖型和非覆盖型描述符对比
Python存取属性方式,通过实例读取属性时,通常返回的是实例中定义的属性;如果实例中没有指定的属性,会获取类属性。而实例中的属性赋值时,通常会在实例中创建属性,不影响类。
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 '<clss {}>'.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:
def __get__(self, instance, owner):
print_args('get', self, instance, owner)
def __set__(self, instance, value):
print_args('set', self, instance, value)
class OverridingNoGet:
def __set__(self, instance, value):
print_args('set', self, instance, value)
class NonOverriding:
def __get__(self, instance, owner):
print_args('get', self, instance, owner)
class Managed:
over = Overriding()
over_no_get = OverridingNoGet()
non_over = NonOverriding()
def spam(self):
print('-> Managed.spam({})'.foramt(display(self)))
覆盖型描述符
obj = Managed()
obj.over
-> Overriding.__get__(<Overriding object>, <Managed object>, <clss Managed>)
Managed.over # instance 托管实例是None
-> Overriding.__get__(<Overriding object>, None, <clss Managed>)
obj.over = 7 # __set__方法
-> Overriding.__get__(<Overriding object>, <Managed object>, 7)
obj.over # 描述符的__get__方法
-> Overriding.__get__(<Overriding object>, <Managed object>, <clss Managed>)
obj.__dict__['over'] = 8 # 跳过描述符,直接通过__dict__属性设值
vars(obj) # 在实例属性中
{'over': 8}
obj.over # Managed.over描述符会覆盖读取obj.over操作
-> Overriding.__get__(<Overriding object>, <Managed object>, <clss Managed>)
没有__get__方法的覆盖型描述符
- 可以只实现__set__方法
只有写操作由描述符处理。
读操作时实例属性会遮盖描述符。
obj.over_no_get # 没有__get__方法,直接返回描述符对象本身
Managed.over_no_get
obj.over_no_get = 7 # 描述符的__set__方法
obj.over_no_get
obj.__dict__['over_no_get'] = 9 # 通过__dict__方法创建同名实例属性
obj.over_no_get # 会直接返回新赋予的值
obj.over_no_get = 7
obj.over_no_get # 仍然是返回新赋予的值'9'
非覆盖型描述符
- 没有实现__set__方法的描述符是非覆盖型描述符
如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性。
obj = Managed()
obj.non_over
obj.non_over = 7
obj.non_over
Managed.non_over
del obj.non_over # 删除实例属性
obj.non_over # 触发的是描述符的__get__方法
在类中覆盖描述符
- 为类属性赋值能覆盖描述符
obj = Managed()
Managed.over = 1
Managed.over_no_get = 2
Managed.non_over = 3
obj.over, obj.over_no_get, obj.non_over
方法是描述符
- 方法是非覆盖型描述符
obj = Managed()
obj.spam # 绑定方法对象
Managed.spam # 函数
obj.spam = 7
obj.spam # 遮盖类属性,返回‘7’
import collections
class Text(collections.UserString):
def __repr__(self):
return 'Text({!r})'.format(self.data)
def reverse(self):
return self[::-1]
word = Text('forward')
word
word.reverse()
Text.reverse('backward') # 在类上调用方法,相当于调用函数
list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')])) # 可以处理Text实例之外的其他对象
Text.reverse.__get__(word) # 得到绑定的实例方法
Text.reverse.__get__(None, Text) # instance为None时,得到函数本身
word.reverse # 与Text.reverse.__get__(word) 一样
word.reverse.__self__
word.reverse.__func__ is Text.reverse
总结
- 描述符的用法,描述符的实例作为另一个类的类属性
- 描述符替换特性
- 描述符用于验证属性,扩展子类用于验证属性是否为空
- 类中计数器生成独一无二的storage_name
- 覆盖型描述符和非覆盖型描述符的差异,实现__set__方法的属于覆盖型描述符
- 方法是非覆盖型描述符。通过实例访问依附在类上的函数时,由描述符协议的处理,就会变成方法。
流畅的Python2015