概览
在Python学习笔记36:动态属性和特性中我们介绍了如何使用特性来“代理”对实例属性的访问,事实上特性是一种特殊的属性描述符。
所谓的属性描述符,是一种实现了描述符协议的特殊类,这个关于属性访问的协议包括__set__\__get__\delete
。
下面我们看下如何实现属性描述符。
实现
我们假设有这么一个订单类:
class Order:
def __init__(self, quantity, price) -> None:
self.quantity = quantity
self.price = price
def total(self):
return self.quantity*self.price
order = Order(1.5, 5)
print(order.total())
# 7.5
显然,订单中的数量(quantity)和单价(price)必须是大于零的数,当然我们可以像Python学习笔记36:动态属性和特性中那样使用特性,但这里我们使用标准的属性描述符。
其实现也很简单,只要实现前面提到的描述符协议,或者部分协议即可。
class PositiveNumber:
def __init__(self, name) -> None:
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError('vlaue mast > 0')
class Order:
quantity = PositiveNumber('quantity')
price = PositiveNumber('price')
def __init__(self, quantity, price) -> None:
self.quantity = quantity
self.price = price
def total(self):
return self.quantity*self.price
order = Order(1.5, 5)
print(order.total())
order2 = Order(0, 3)
print(order2.total())
# 7.5
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note37\test.py", line 29, in <module>
# order2 = Order(0, 3)
# File "D:\workspace\python\python-learning-notes\note37\test.py", line 20, in __init__
# self.quantity = quantity
# File "D:\workspace\python\python-learning-notes\note37\test.py", line 12, in __set__
# raise ValueError('vlaue mast > 0')
# ValueError: vlaue mast > 0
具体的描述符协议包括下面三个协议方法:
__get__(self, instatnce, owner)
__set__(self, instance, value)
__delete__(self, instance)
这里仅实现了__get__
和__set__
,属性描述符通常会实现这两个方法,或者其中之一,__delete__
并不常用。
这里是实现协议,并非是基类的抽象方法,所以不需要继承,更不需要强制实现所有协议方法。有关协议的灵活性我们在Python学习笔记28:从协议到抽象基类中有过讨论。
协议方法中的self
和普通的类没有区别,就是指属性描述符实例本身。instance
为属性描述符绑定的目标类的实例,owner
为属性描述符绑定的类的引用。
这里或许会觉得
__get__
中会持有一个绑定类引用owner
很突兀,而且好像没什么用,但后面会说明其用途。
这里我们创建了一个属性描述符PositiveNumber
,其用途和我们在Python学习笔记36:动态属性和特性中创建的特性工厂函数极为相似,后者是创建一个只能设置为正数的特性,这里是创建一个只能设置为正数的属性描述符实例。
属性描述符的内部定义很简单,这里不过多赘述。
- 需要注意的是,在读和写的时候,我们都是将真实的数据存储在
instance.__dict__
中,而非是属性描述符实例self.__dict__
中,这是因为虽然在这个具体示例中我们的属性描述符PositiveNumber
的两个实例PositiveNumber('quantity')
和PositiveNumber('price')
仅仅绑定到了Order
类,但PositiveNumber
这个属性描述符创建后其实是可以绑定到任意需要使用类似的访问控制的类中的,所以从这点来说,属性描述符只是用于对绑定到的具体类实例的具体属性的访问控制,当然具体读写的属性存储在绑定的目标实例中,而非属性描述符自己的实例。- 这里使用
instance.__dict__
而非getattr(instance,name)
是因为后者会再次触发属性描述符,陷入无限递归。
将属性描述符绑定到目标类和在类中用特性工厂函数创建特性也没有区别,都是在类定义中给相应的类属性进行赋值。
绑定好属性描述符以后,所有对Order
类实例的quantity
和price
属性的读写操作都将由绑定的属性描述符处理,这点和特性没有区别,因为特性就是特殊的属性描述符。
既然特性是特殊的属性描述符,那属性描述符和特性有什么区别?
和特性的区别
总的来说,特性可以看做是Python定义好的一个特定用途的属性描述符,而用户自定义的属性描述符比特性相对更为灵活,可以根据需要进行抽象和继承,构建灵活用途的属性描述符。
假设我们现在需要给订单添加一个属性用于描述物品信息,并且需要验证这个字符串不能是空字符串。
import abc
from typing import ValuesView