PEP-593 添加了 typing.Annotated,作为向现有类型添加特定上下文元数据的
方式,并规定 Annotated[T, x] 应被任何没有针对 x 特殊逻辑
的工具或库视为 T。
本软件包提供了元数据对象,可用于表示常见的约束,例如标量值和集合大小的上限和下限,
一个用于运行时检查的 Predicate 标记,以及有关我们打算如何解释这些元数据的
描述。在某些情况下,我们还会指出不需要此软件包的替代表示形式。
pip install annotated-typesfrom typing import Annotated
from annotated_types import Gt, Len, Predicate
class MyClass:
age: Annotated[int, Gt(18)] # 有效: 19, 20, ...
# 无效: 17, 18, "19", 19.0, ...
factors: list[Annotated[int, Predicate(is_prime)]] # 有效: 2, 3, 5, 7, 11, ...
# 无效: 4, 8, -2, 5.0, "prime", ...
my_list: Annotated[list[int], Len(0, 10)] # 有效: [], [10, 20, 30, 40, 50]
# 无效: (1, 2), ["abc"], [0] * 20虽然 annotated-types 为了性能避免了运行时检查,但用户不应构造无效组合,例如 MultipleOf("non-numeric") 或 Annotated[int, Len(3)]。
下游实现者可以根据情况选择引发错误、发出警告、静默忽略
元数据项等,如果下面描述的元数据对象与不兼容的类型一起使用 - 或出于其他任何原因!
表示可排序值(可能是数字、日期、时间、字符串、集合等)的包含式和/或排他式边界。
请注意,边界值不一定是已注解的类型,只要它们可以进行比较即可:例如,Annotated[int, Gt(1.5)]
是有效的,意味着该值是一个整数 x,使得 x > 1.5。
我们建议实现者也可以将 functools.partial(operator.le, 1.5)
解释为等同于 Gt(1.5),供希望避免对 annotated-types
软件包的运行时依赖的用户使用。
明确来说,这些类型具有以下含义:
Gt(x)- 值必须“大于”x- 等同于排他式最小值Ge(x)- 值必须“大于等于”x- 等同于包含式最小值Lt(x)- 值必须“小于”x- 等同于排他式最大值Le(x)- 值必须“小于等于”x- 等同于包含式最大值
Interval(gt, ge, lt, le) 允许您使用一个元数据对象指定上限和下限。
None 属性应被忽略,而非 None 属性则按上述单个边界处理。
MultipleOf(multiple_of=x) 可能有两种解释:
- Python 语义,意味着
value % multiple_of == 0,或 - JSON schema 语义,
其中
int(value / multiple_of) == value / multiple_of。
我们鼓励用户注意这两种常见的解释及其不同的行为,特别是考虑到非常大的数字 或非整数数字很容易因浮点不精确性导致静默数据损坏。
我们鼓励库仔细记录它们实现的解释。
Len() 意味着 min_length <= len(value) <= max_length - 下限和上限是包含式的。
除了可以选择包含上限和下限的 Len() 外,我们还提供了
MinLen(x) 和 MaxLen(y),它们分别等同于 Len(min_length=x)
和 Len(max_length=y)。
Len, MinLen, 和 MaxLen 可与任何支持 len(value)
的类型一起使用。
使用示例:
Annotated[list, MaxLen(10)](或Annotated[list, Len(max_length=10)]) - 列表长度必须为 10 或更少Annotated[str, MaxLen(10)]- 字符串长度必须为 10 或更少Annotated[list, MinLen(3)](或Annotated[list, Len(min_length=3)]) - 列表长度必须为 3 或更多Annotated[list, Len(4, 6)]- 列表长度必须为 4、5 或 6Annotated[list, Len(8, 8)]- 列表长度必须恰好为 8
min_inclusive已重命名为min_length,含义不变max_exclusive已重命名为max_length,上限现在是包含式而不是排他式- 已移除切片被解释为
Len的建议,因为切片和Len的上限含义不同且存在歧义
有关讨论,请参阅 issue #23。
Timezone 可与 datetime 或 time 一起使用,以表达允许的时区。
Annotated[datetime, Timezone(None)] 必须是朴素的 datetime。
Timezone[...] (字面上的省略号)
表示允许任何带时区的 datetime。您还可以传递一个特定的时区字符串或
tzinfo 对象,例如 Timezone(timezone.utc) 或 Timezone("Africa/Abidjan"),
以表示只允许特定时区,但我们注意到这通常是脆弱设计的症状。
Unit(unit: str) 表示已注解的数值是具有指定单位的量的量级。
例如,Annotated[float, Unit("m/s")]
将是一个表示米/秒速度的浮点数。
请注意,annotated_types 本身不尝试以任何方式解析或验证
单位字符串。这完全留给下游库,例如 pint 或
astropy.units。
关于库如何使用此元数据的一个示例:
from annotated_types import Unit
from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args
# 给定一个带有单位注解的类型:
Meters = Annotated[float, Unit("m")]
# 您可以使用任何接受字符串并返回所需类型的可调用对象,将注解转换为特定的单位类型
# 可调用对象
T = TypeVar("T")
def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
if get_origin(tp) is Annotated:
for arg in get_args(tp):
if isinstance(arg, Unit):
return unit_cls(arg.unit)
return None
# 使用 `pint`
import pint
pint_unit = cast_unit(Meters, pint.Unit)
# 使用 `astropy.units`
import astropy.units as u
astropy_unit = cast_unit(Meters, u.Unit)Predicate(func: Callable) 表示对于有效值,func(value)
是真值。用户应优先使用上面可静态检查的元数据,但如果您需要任意运行时谓词的
全部功能和灵活性... 您就在这里。
对于一些常见的约束,我们提供了泛型类型:
LowerCase = Annotated[T, Predicate(str.islower)]UpperCase = Annotated[T, Predicate(str.isupper)]IsDigit = Annotated[T, Predicate(str.isdigit)]IsFinite = Annotated[T, Predicate(math.isfinite)]IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]IsNan = Annotated[T, Predicate(math.isnan)]IsNotNan = Annotated[T, Predicate(Not(math.isnan))]IsInfinite = Annotated[T, Predicate(math.isinf)]IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]
这样您就可以写例如 x: IsFinite[float] = 2.0,而不是更长的
(但完全等价的)x: Annotated[float, Predicate(math.isfinite)] = 2.0。
一些库可能会有特殊的逻辑来处理已知或可理解的谓词,
例如通过检查 str.isdigit 并利用其存在来调用自定义逻辑来强制执行仅数字字符串,
并自定义一些生成的外部模式。因此,鼓励用户避免使用像 lambda s: s.lower()
这样的间接方式,而倾向于使用如 str.lower 或 re.compile("pattern").search
这样的可内省方法。
为了能够在不引入导致实现者无法内省谓词的内省的情况下,实现对
math.isnan 等常用谓词的基本否定,我们提供了一个 Not
包装器,它以可内省的方式简单地否定了谓词。上面列出的几个谓词就是这样创建的。
我们不指定对于引发异常的谓词应该期望的行为。
例如,Annotated[int, Predicate(str.isdigit)] 可能会静默跳过无效约束,
或者静态引发错误;或者它可能会尝试调用它,然后传播或丢弃由此产生的
TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object
异常。我们鼓励库记录它们选择的行为。
doc() 可用于在 Annotated 中添加文档信息,用于函数和方法参数、变量、类属性、返回类型以及任何可以使用 Annotated 的地方。
它期望一个可静态分析的值,因为主要用例是针对静态分析、编辑器、文档生成器和类似工具。
它返回一个 DocInfo 类,其中包含一个名为 documentation 的属性,其中包含传递给 doc() 的值。
这是 typing-doc 提案 的早期采用者替代形式。
实现者可以选择提供一个便利的包装器来组合多个元数据。
这有助于减少用户代码的冗长和认知开销。
例如,Pydantic 这样的实现者可能提供一个 Field 或 Meta 类型,
它接受关键字参数并将其转换为低级元数据:
from dataclasses import dataclass
from typing import Iterator
from annotated_types import GroupedMetadata, Ge
@dataclass
class Field(GroupedMetadata):
ge: int | None = None
description: str | None = None
def __iter__(self) -> Iterator[object]:
# 迭代 GroupedMetadata 对象应产生描述它的 annotated-types
# 约束元数据对象,尽可能完整,
# 也可能包含其他未知对象。
if self.ge is not None:
yield Ge(self.ge)
if self.description is not None:
yield Description(self.description)消费 annotated-types 约束的库应检查 GroupedMetadata 并通过迭代对象进行解包,
将结果视为已在 Annotated 类型中“解包”。
相同的逻辑也应应用于 PEP 646 Unpack 类型,
以便 Annotated[T, Field(...)]、Annotated[T, Unpack[Field(...)]] 和
Annotated[T, *Field(...)] 都被一致地对待。
消费 annotated-types 的库也应忽略任何它们不认识的、来自解包 GroupedMetadata
的元数据,就像它们忽略 Annotated 本身的、不可识别的元数据一样。
我们自己的 annotated_types.Interval 类是一个 GroupedMetadata,
它将自身解包为 Gt、Lt 等。因此,这并非抽象问题。
类似地,annotated_types.Len 是一个 GroupedMetadata,
它将自身解包为(可选的)MinLen 和 MaxLen。
我们无意规定元数据和约束的*使用*方式,但作为如何从类型注解中解析约束的一个例子,
请参阅我们在 test_main.py 中的实现。
由实现者决定如何使用此元数据。 您可以将元数据用于运行时类型检查、生成模式或生成示例数据等用途。
本软件包由 Pydantic 和 Hypothesis 的维护者在 PyCon 2022 sprints 活动中设计,目标是尽可能方便终端用户为运行时库提供更具信息量的注解。
它刻意保持最小化,并遵循 PEP-593,允许下游在支持(什么)方面有相当大的自由裁量权。 尽管如此,我们预计保持简单并*只*涵盖最常见的用例,将为用户和维护者提供我们所能提供的最佳体验。 如果您需要为您的类型添加更多约束 - 请遵循我们的方法,通过定义它们并记录它们!