现在,让我们从最基本的示例开始,然后逐步添加新功能。
Ten 类是一个描述器,其 get() 方法总是返回常量 10:
class Ten:
def __get__(self, obj, objtype=None):
return 10
要使用描述器,它必须作为一个类变量存储在另一个类中:
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
用交互式会话查看普通属性查找和描述器查找之间的区别:
>>>
>>> a = A() # Make an instance of class A
>>> a.x # Normal attribute lookup
5
>>> a.y # Descriptor lookup
10
在 a.x 属性查找中,点运算符会找到存储在类字典中的 ‘x’: 5。 在 a.y 查找中,点运算符会根据描述器实例的 get 方法将其识别出来,调用该方法并返回 10 。
请注意,值 10 既不存储在类字典中也不存储在实例字典中。相反,值 10 是在调用时才取到的。
这个简单的例子展示了一个描述器是如何工作的,但它不是很有用。在查找常量时,用常规属性查找会更好。
在下一节中,我们将创建更有用的东西,即动态查找。
有趣的描述器通常运行计算而不是返回常量:
import os
class DirectorySize:
def __get__(self, obj, objtype=None):
return len(os.listdir(obj.dirname))
class Directory:
size = DirectorySize() # Descriptor instance
def __init__(self, dirname):
self.dirname = dirname # Regular instance attribute
交互式会话显示查找是动态的,每次都会计算不同的,经过更新的返回值:
>>>
>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size # The songs directory has twenty files
20
>>> g.size # The games directory has three files
3
>>> os.remove('games/chess') # Delete a game
>>> g.size # File count is automatically updated
2
除了说明描述器如何运行计算,这个例子也揭示了 get() 参数的目的。形参 self 接收的实参是 size,即 DirectorySize 的一个实例。形参 obj 接收的实参是 g 或 s,即 Directory 的一个实例。而正是 obj 让 get() 方法获得了作为目标的目录。形参 objtype 接收的实参是 Directory 类。
描述器的一种流行用法是托管对实例数据的访问。描述器被分配给类字典中的公开属性,而实际数据作为私有属性存储在实例字典中。当访问公开属性时,会触发描述器的 get() 和 set() 方法。
在下面的例子中,age 是公开属性,_age 是私有属性。当访问公开属性时,描述器会记录下查找或更新的日志:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAgeAccess:
def __get__(self, obj, objtype=None):
value = obj._age
logging.info('Accessing %r giving %r', 'age', value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', 'age', value)
obj._age = value
class Person:
age = LoggedAgeAccess() # Descriptor instance
def __init__(self, name, age):
self.name = name # Regular instance attribute
self.age = age # Calls __set__()
def birthday(self):
self.age += 1 # Calls both __get__() and __set__()
交互式会话展示中,对托管属性 age 的所有访问都被记录了下来,但常规属性 name 则未被记录:
>>>
>>> mary = Person('Mary M', 30) # The initial age update is logged
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40
>>> vars(mary) # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}
>>> mary.age # Access the data and log the lookup
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday() # Updates are logged as well
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31
>>> dave.name # Regular attribute lookup isn't logged
'David D'
>>> dave.age # Only the managed attribute is logged
INFO:root:Accessing 'age' giving 40
40
此示例的一个主要问题是私有名称 _age 在类 LoggedAgeAccess 中是硬耦合的。这意味着每个实例只能有一个用于记录的属性,并且其名称不可更改。
当一个类使用描述器时,它可以告知每个描述器使用了什么变量名。
在此示例中, Person 类具有两个描述器实例 name 和 age。当类 Person 被定义的时候,他回调了 LoggedAccess 中的 set_name() 来记录字段名称,让每个描述器拥有自己的 public_name 和 private_name:
import logging
logging.basicConfig(level=logging.INFO)
class LoggedAccess:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
logging.info('Accessing %r giving %r', self.public_name, value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', self.public_name, value)
setattr(obj, self.private_name, value)
class Person:
name = LoggedAccess() # First descriptor instance
age = LoggedAccess() # Second descriptor instance
def __init__(self, name, age):
self.name = name # Calls the first descriptor
self.age = age # Calls the second descriptor
def birthday(self):
self.age += 1
交互交互式会话显示类 Person 调用了 set_name() 方法来记录字段的名称。在这里,我们调用 vars() 来查找描述器而不触发它:
>>>
>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}
现在,新类会记录对 name 和 age 二者的访问:
>>>
>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> kate = Person('Catherine C', 20)
INFO:root:Updating 'name' to 'Catherine C'
INFO:root:Updating 'age' to 20
这两个 Person 实例仅包含私有名称:
>>>
>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}
descriptor 就是任何一个定义了 get(),set() 或 delete() 的对象。
可选地,描述器可以具有 set_name() 方法。这仅在描述器需要知道创建它的类或分配给它的类变量名称时使用。(即使该类不是描述器,只要此方法存在就会调用。)
在属性查找期间,描述器由点运算符调用。如果使用 vars(some_class)[descriptor_name] 间接访问描述器,则返回描述器实例而不调用它。
描述器仅在用作类变量时起作用。放入实例时,它们将失效。
描述器的主要目的是提供一个挂钩,允许存储在类变量中的对象控制在属性查找期间发生的情况。
传统上,调用类控制查找过程中发生的事情。描述器反转了这种关系,并允许正在被查询的数据对此进行干涉。
描述器的使用贯穿了整个语言。就是它让函数变成绑定方法。常见工具诸如 classmethod(), staticmethod(),property() 和 functools.cached_property() 都作为描述器实现。
在此示例中,我们创建了一个实用而强大的工具来查找难以发现的数据损坏错误。
验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。
这个 Validator 类既是一个 abstract base class 也是一个托管属性描述器。
from abc import ABC, abstractmethod
class Validator(ABC):
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
自定义验证器需要从 Validator 继承,并且必须提供 validate() 方法以根据需要测试各种约束。
这是三个实用的数据验证工具:
OneOf 验证值是一组受约束的选项之一。
Number 验证值是否为 int 或 float。根据可选参数,它还可以验证值在给定的最小值或最大值之间。
String 验证值是否为 str。根据可选参数,它可以验证给定的最小或最大长度。它还可以验证用户定义的 predicate。
class OneOf(Validator):
def __init__(self, *options):
self.options = set(options)
def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
class Number(Validator):
def __init__(self, minvalue=None, maxvalue=None):
self.minvalue = minvalue
self.maxvalue = maxvalue
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
if self.minvalue is not None and value < self.minvalue:
raise ValueError(
f'Expected {value!r} to be at least {self.minvalue!r}'
)
if self.maxvalue is not None and value > self.maxvalue:
raise ValueError(
f'Expected {value!r} to be no more than {self.maxvalue!r}'
)
class String(Validator):
def __init__(self, minsize=None, maxsize=None, predicate=None):
self.minsize = minsize
self.maxsize = maxsize
self.predicate = predicate
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f'Expected {value!r} to be an str')
if self.minsize is not None and len(value) < self.minsize:
raise ValueError(
f'Expected {value!r} to be no smaller than {self.minsize!r}'
)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError(
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
)
if self.predicate is not None and not self.predicate(value):
raise ValueError(
f'Expected {self.predicate} to be true for {value!r}'
)
这是在真实类中使用数据验证器的方法:
class Component:
name = String(minsize=3, maxsize=10, predicate=str.isupper)
kind = OneOf('wood', 'metal', 'plastic')
quantity = Number(minvalue=0)
def __init__(self, name, kind, quantity):
self.name = name
self.kind = kind
self.quantity = quantity
描述器阻止无效实例的创建:
>>>
>>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
...
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
>>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled
Traceback (most recent call last):
...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
>>> Component('WIDGET', 'metal', -5) # Blocked: -5 is negative
Traceback (most recent call last):
...
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number
Traceback (most recent call last):
...
TypeError: Expected 'V' to be an int or float
>>> c = Component('WIDGET', 'metal', 5) # Allowed: The inputs are valid