In [50]:
class Quantity: # 描述符类
def __init__(self, storage_name): # storage_name 是托管实例中存储值的属性的名称
self.storage_name = storage_name
# 设置托管属性赋值会调用 __set__方法
# 这里的 self 是描述符实例,即 LineItem.weight 或 LineItem.price
# instance 是托管实例(LineItem 实例),value 是要设定的值
def __set__(self, instance, value):
if value > 0:
# 这里必须设值 __dict__ 属性,如果使用内置的 setattr 会再次调用 __set__ 无限递归
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be > 0')
class LineItem: # 托管类
weight = Quantity('weight') # 第一个描述符实例绑定到 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
上面的读值方法不需要特殊逻辑,所以 Quantity 类不需要定义 __get__ 方法
In [51]:
truffle = LineItem('White truffle', 100, 10)
truffle.weight # 其实是通过 Quantitiy.__get__ 方法返回的
Out[51]:
In [52]:
truffle.__dict__['weight'] = 13 # 真实值存在这里,用 Quantitiy 类实例覆盖了它
truffle.weight
Out[52]:
In [53]:
truffle = LineItem('White truffle', 100, 0) # 代码正常运行,禁止 0 美元
编写 __set__ 方法时,要记住 self 和 instance 参数的意思:self 是描述符实例,instance 是托管实例。管理实例属性的描述符应该把值存到托管实例中,因此,Python 才为描述符中的那个方法提供了 instance 参数
你可能想把各个托管属性的值直接存在描述符,但是这种做法是错误的。也就是说,在 __set__ 方法中,应该这么写:
instance.__dict__[self.storage_name] = value
而不能试图下面这种错误的写法:
self.__dict__[self.storage_name] = value
因为 self 是描述符实例,它其实是托管类(LineItem)的属性,同一时刻,内存中可能有几个 LineItem 实例,不过只会有两个描述符实例:LineItem.weight 和 LineItem.price(因为这是类属性而不是实例属性)。因此,存储在描述符实例中的数据,其实会变成 LineItem 类的类属性,从而由全部 LineItem 实例共享
上面有个缺点,在托管类的定义体中实例化描述符时要重复输入属性的名称。如果 LineItem 类像下面这样声明就好了。
class LineItem:
weight = Quantity()
price = Quantity()
...
但问题是,赋值语句右手边表达式先执行,此时变量还不存在,Quantity() 表达式计算的结果是创建描述符实例,而此时 Quantity 类中的代码无法猜出要把描述符绑定给哪个变量(例如 weight 或 price)
因此必须明确指明各个 Quantity 实例的名称,这么不仅麻烦,而且危险,如果程序员直接复制粘贴而忘记了编辑名称,例如 price = Quantity('weight') 就会出大事
下面我们先介绍一个不太优雅的解决方案,更优雅的下章介绍
我们不用管用户传什么名称,每个 Quantity 描述符有独一无二的 storage_name 就可以了
In [54]:
class Quantity:
__counter = 0 #类变量,为了为不同的实例创建不同的 sorage_name
def __init__(self):
cls = self.__class__ # Quantity 类的引用
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index) #独一无二的 storage_name
cls.__counter += 1
# 因为托管属性名与 storage_name 不同,我们要实现 __get__ 方法
# 稍后说明 owner 参数
def __get__(self, instance, owner):
return getattr(instance, self.storage_name) # 使用内置的 getattr 从 instance 获取值
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value) # 使用内置的 setattr 向 instance 设置值
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
这里可以使用 getattr 函数和 setattr 获取值,无需使用 instance.dict,因为托管属性和存储属性名称不同
In [55]:
coconuts = LineItem('Brazilian coconut', 20, 17.95)
coconuts.weight, coconuts.price
Out[55]:
In [56]:
getattr(coconuts, '_Quantity#0'), getattr(coconuts, '_Quantity#1')
Out[56]:
get 方法有 3 个参数,self, instance 和 owner。owner 参数是托管类(如 LineItem)的引用(注意是类而不是实例,instance 是类的实例),通过描述符从托管类中获取属性时用得到。
如果使用 LineItem.weight 从类中获取托管属性,描述符 __get__ instance 参数收到的值是 None,因此会抛出 AttributeError 异常
In [57]:
LineItem.weight
抛出 AttributeError 异常是实现 __get__ 方法方式之一,如果选择这么做,应该修改错误信息,去掉令人困惑的 NoneType 和 _Quantity#0,改成 'LineItem' class
has no such attribute 更好。最好能给出缺少的属性名,但是在这里描述符不知道托管属性的名称,所以只能做到这样
此外,为了个用户提供内省和其它元编程技术支持,通过类访问托管属性时,最高让 __get__ 方法返回描述符实例。下面对 __get__ 做了一些改动
In [61]:
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:
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
In [65]:
coconuts = LineItem('Brazilian coconut', 20, 17.95)
LineItem.weight
Out[65]:
In [64]:
coconuts.price
Out[64]:
看了上面例子,你可能觉得为了管理几个描述符写这么多代码不值得,但是开发框架的话,描述符会在一个单独的实用工具模块中定义,以便在整个应用中使用,就很值得了
import model_v4c as model
class LineItem:
weight = model.Quantity()
price = model.Quantity()
...
就像上面这样,把描述符放到单独模块中。现在来说,Quantity 描述符能出色完成工作,唯一缺点是,出餐属性的名称是生成的(如 _Quantity#0),导致难以调试,如果想自动把出餐属性的名称设为与托管属性的名称类似,需要使用到类装饰器或元类,下章讨论
我们上一章的特性工厂函数其实也很容易实现与描述符同样的功能,如下
In [66]:
def quantity(storage_name):
try:
quantity.counter += 1
except AttributeError:
quantity.counter = 0 # 第一次赋值
# 借助闭包每次创建不同的 storage_name
storage_name = '_{}:{}'.format('quantity', quantity.counter)
def qty_getter(instance):
return instance.__dict__[storage_name]
def qty_setter(instance, value):
if value > 0:
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)
In [70]:
import abc
class 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, 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): # 抽象类,也继承自 AutoStorage
def __set__(self, instance, value):
# __set__ 方法把验证委托给 validate 方法
value = self.validate(instance, value)
#返回的 value 值返回给超类的 __set__ 方法,存储值
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value): # 抽象方法
'''return validated value or raise ValueError'''
class Quantity(Validated):
'''a number greater than zero'''
# 只需要根据不同的验证规则实现 validate 方法即可
def validate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
return value
class NonBlank(Validated):
'''a string with at least one not-space character'''
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value
class LineItem: # 托管类
weight = Quantity()
price = Quantity()
description = NonBlank()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
In [71]:
coconuts = LineItem('Brazilian coconut', 20, 17.95)
coconuts.description, coconuts.weight, coconuts.price
Out[71]:
In [72]:
coconuts = LineItem(' ', 20, 17.95)
本章所举的几个 LineItem 实例演示了描述符的典型用途 -- 管理数据属性。这种描述符也叫覆盖型描述符,因为描述符的 __set__ 方法使用托管实例中同名属性覆盖(即插手接管)了要设置的属性,不过也有非覆盖型描述符,下节介绍两种区别
如前面所说,Python 存取属性方式特别不对等,通过实例读取属性,通常返回是实例中定义的属性名,但是如果实例中没有指定的属性,那么会获取类属性,而为实例中属性赋值时,通常会在实例中创建属性,根本不影响类
这种不对等处理方式对描述符也有影响,其实根据是否定义 __set__ 方法,描述符行为差异,我们需要几个类(下面的 print_args 是为了显示好看,cls_name 和 display 是辅助函数,这几个函数没必要研究):
In [1]:
## 辅助函数,仅用于显示 ##
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:
'''也称数据描述符或强制描述符'''
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, owner):
print_args('set', self, instance, owner)
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({})'.format(display(self)))
In [2]:
obj = Managed()
obj.over # get 方法,第二个参数是托管实例 obj
In [3]:
Managed.over #第二个参数是 None
In [4]:
obj.over = 7 # 触发描述符的 __set__ 方法,最后一个参数是 7
In [5]:
obj.over # 仍然触发描述符的 __get__ 方法
In [6]:
obj.__dict__['over'] = 8 # 直接通过 obj.__dict__ 属性赋值
vars(obj) #确认值在 obj.__dict__ 下
Out[6]:
In [7]:
obj.over # 即使有名为 over 的实例属性,Managed.over 描述符仍然会覆盖读取 obj.over 操作
In [8]:
obj.over_no_get
Out[8]:
In [9]:
Managed.over_no_get
Out[9]:
In [10]:
obj.over_no_get = 7
In [11]:
obj.over_no_get
Out[11]:
In [12]:
obj.__dict__['over_no_get'] = 9
obj.over_no_get
Out[12]:
In [13]:
obj.over_no_get = 7
In [14]:
obj.over_no_get
Out[14]:
In [15]:
obj = Managed()
obj.non_over
In [17]:
obj.non_over = 7
obj.non_over
Out[17]:
In [19]:
Managed.non_over
In [20]:
del obj.non_over
obj.non_over
In [21]:
obj = Managed()
Managed.over = 1 # 覆盖了描述符
Managed.over_no_get = 2
Managed.non_over = 3
obj.over, obj.over_no_get, obj.non_over
Out[21]:
In [22]:
obj = Managed()
obj.spam # 获取的是绑定方法对象
Out[22]:
In [23]:
Managed.spam # 获取的是函数
Out[23]:
In [24]:
obj.spam = 7 # 遮盖类属性,导致无法通过 obj.spam 访问 spam 方法
obj.spam
Out[24]:
函数没有实现 __set__ 方法,因此是非覆盖型描述符。
从上面能看出一个信息,obj.spam 和 Managed.spam 获取的是不同的对象,与描述符一样,通过托管类访问时,函数的 __get__ 方法会返回自身的引用。但是通过实例访问时,函数的 __get__ 方法返回的是绑定方法对象,一种可调用的对象,里面包装着函数,并把托管实例(如 obj)绑定给函数的第一个参数(即 self),这与 functools.partial 函数行为一致
In [25]:
import collections
class Text(collections.UserString):
def __repr__(self):
return 'Text({!r})'.format(self.data)
def reverse(self):
return self[::-1]
In [26]:
word = Text('forward')
word
Out[26]:
In [27]:
word.reverse()
Out[27]:
In [34]:
Text.reverse(Text('backward')) # 在类上调用方法相当于调用函数
Out[34]:
In [28]:
type(Text.reverse), type(word.reverse) # 类型不相同,一个 function,一个 method
Out[28]:
In [35]:
list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')])) # Text.reverse 相当于函数,甚至可以处理 Text 实例外其它对象
Out[35]:
In [30]:
Text.reverse.__get__(word) # 函数都是非覆盖型描述符。在函数上调用 __get__ 方法传入实例,得到的是绑定到那个实例的方法
Out[30]:
In [31]:
word.reverse # 其实会调用 Text.reverse.__get__(word) 方法,返回对应绑定方法。
Out[31]:
In [32]:
word.reverse.__self__ # 绑定放方法对象有个 __self__ 属性,其值是调用这个方法的实例引用
Out[32]:
In [33]:
word.reverse.__func__ is Text.reverse # 绑定方法的 __func__ 是依附在托管类上的原始函数引用
Out[33]:
In [ ]: