描述符协议允许我们完全定制属性访问。 Python 的文档描述了用文字描述的类型的协议。 让我们看看我们如何把这些写成类型提示。
描述符示例
让我们为这个完整的描述符添加类型提示,它验证了只允许正数。
import math
class PositiveFloat:
def __set_name__(self, owner, name) -> None:
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__[self.name]
def __set__(self, obj, value):
if value <= 0.0 or math.isnan(value) or math.isinf(value):
raise ValueError(f"{self.name} must be a positive real number.")
obj.__dict__[self.name] = value
def __delete__(self, obj) -> None:
if self.name not in obj.__dict__:
raise AttributeError(self.name)
del obj.__dict__[self.name]
快速提醒一下所有这些方法。
-
__set_name__()当我们的描述符被分配到一个类的变量时被调用,它允许我们的描述符实例知道它被分配到哪个名字。 -
__get__()是在属性访问时调用的。当在类上访问时, 是 ,在这种情况下,返回描述符实例是正常的。当在实例上访问时, 包含该实例。objNoneobj我们的描述符把它的值存储在实例
__dict__,所以它的实现是直接的。 -
__set__()在为属性设置新的值时被调用。 就是这个实例。obj -
__delete__()在删除属性时被调用。 再次成为实例。obj
描述符不需要实现所有这些方法。 我们在这里只是用一个完整的例子来涵盖所有的类型提示。
我们可以用以下方法检查我们的描述符的运行情况 python -i example.py:
>>> class Widget:
... rotations = PositiveFloat()
...
>>>
>>> widget = Widget()
>>> # Test set, get, delete happy paths
>>> widget.rotations = 12.0
>>> widget.rotations
12.0
>>> del widget.rotations
>>> # Setting to an invalid value:
>>> widget.rotations = -1.0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../example.py", line 15, in __set__
raise ValueError(f"{self.name} must be a positive real number.")
ValueError: rotations must be a positive real number.
>>> # Deleting when it’s already been deleted:
>>> del widget.rotations
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../example.py", line 20, in __delete__
raise AttributeError(self.name)
AttributeError: rotations
好的很好,现在他们的类型提示怎么样了?
Addimus类型提示
好吧,当我们添加类型提示时,我们得到:
from __future__ import annotations
import math
from typing import cast, overload
class PositiveFloat:
def __set_name__(self, owner: type[object], name: str) -> None:
self.name = name
@overload
def __get__(self, obj: None, objtype: None) -> PositiveFloat:
...
@overload
def __get__(self, obj: object, objtype: type[object]) -> float:
...
def __get__(
self, obj: object | None, objtype: type[object] | None = None
) -> PositiveFloat | float:
if obj is None:
return self
return cast(float, obj.__dict__[self.name])
def __set__(self, obj: object, value: float) -> None:
if value <= 0.0 or math.isnan(value) or math.isinf(value):
raise ValueError(f"{self.name} must be a positive real number.")
obj.__dict__[self.name] = value
def __delete__(self, obj: object) -> None:
if self.name not in obj.__dict__:
raise AttributeError(self.name)
del obj.__dict__[self.name]
让我们来看看这个:
-
在
__set_name__(),owner参数是描述符被分配到的类。我们知道它是一个类,但没有别的,所以我们可以用type[object]来表示 "从object派生的任何类",也就是 "任何类"。name是属性的名称,我们不需要返回任何东西。 -
__get__()这是最复杂的。它需要使用overload(),以准确地涵盖它的两种调用方式。 我们写出了在类( )和实例( )上调用的两种情况。然后实现将这两种情况的类型结合起来。obj: None``obj: object我们需要在我们的
return语句中使用cast(),因为Mypy无法判断__dict__中的属性必须是float。它假设从__dict__中获取的动态属性可能是Any。这是一个合理的假设,但是从我们描述符的实现中我们知道值应该是一个float。(...除非一些顽皮的代码直接修改__dict__)。如果没有
cast(),我们会得到这个错误。example.py:24: error: Returning Any from function declared to return "Union[PositiveFloat, float]" -
__set__()和__delete__()都是比较直接的。我们声明实例可以是任何类型,并且设置值必须是float。
有限的所有者类
在整个类型提示中,我们用type[object] 表示我们的描述符所依附的类型,用object 表示实例的类型。这是因为我们的描述符不以依附于任何特定的类型为目标。
如果我们想限制描述符对特定类型的依附,我们只需要编辑每个类型的提示,将object 换成该类型。例如,设想我们引入了一个类Validatable 。我们将编辑__set_name__() 中的类型,读作。
def __set_name__(self, owner: type[Validatable], name: str) -> None:
...
其他的方法也是如此。
然后,如果我们将我们的描述符分配给一个非Validatable 类。
class Widget:
rotations = PositiveFloat()
widget = Widget()
widget.rotations = 12.0
Mypy会抱怨说overload 的情况不匹配。
$ mypy --strict example.py
example.py:46: error: No overload variant of "__get__" of "PositiveFloat" matches argument types "Widget", "Type[Widget]"
example.py:46: note: Possible overload variants:
example.py:46: note: def __get__(self, obj: None, objtype: None) -> PositiveFloat
example.py:46: note: def __get__(self, obj: Validatable, objtype: Type[Validatable]) -> float
example.py:46: error: Argument 1 to "__set__" of "PositiveFloat" has incompatible type "Widget"; expected "Validatable"
Found 2 errors in 1 file (checked 1 source file)
请注意,Mypy在类的定义中没有看到任何问题,只是在我们使用实例的时候。