跳转到主要内容

精确且灵活的类型提示构建块

项目描述

optype

精确且灵活的类型提示构建块。

optype - PyPI optype - Python Versions optype - license

optype - CI optype - pre-commit optype - basedmypy optype - basedpyright optype - ruff


安装

Optype可在PyPI上作为optype使用

pip install optype

为了可选的NumPy支持,建议使用numpy扩展。这确保安装的numpy版本与optype兼容,遵循NEP 29SPEC 0

pip install "optype[numpy]"

有关更多信息,请参阅optype.numpy文档

示例

假设您正在编写一个twice(x)函数,该函数评估2 * x。实现它很简单,但类型注解怎么办?

因为 twice(2) == 4twice(3.14) == 6.28 以及 twice('I') = 'II',所以看起来把它写成 twice[T](x: T) -> T: ... 是一个好主意。然而,这样并不能包括像 twice(True) == 2twice((42, True)) == (42, True, 42, True) 这样的情况,其中输入和输出类型是不同的。此外,twice 应该接受任何带有接受 2 作为参数的 __rmul__ 方法的自定义类型。

这就是 optype 有用的地方,它为所有内置的特殊方法提供单方法协议。对于 twice,我们可以使用 optype.CanRMul[T, R],正如名称所示,这是一个只有 def __rmul__(self, lhs: T) -> R: ... 方法的协议。有了这个,twice 函数可以这样编写

Python 3.10 Python 3.12+
from typing import Literal
from typing import TypeAlias, TypeVar
from optype import CanRMul

R = TypeVar('R')
Two: TypeAlias = Literal[2]
RMul2: TypeAlias = CanRMul[Two, R]

def twice(x: RMul2[R]) -> R:
    return 2 * x
from typing import Literal
from optype import CanRMul

type Two = Literal[2]
type RMul2[R] = CanRMul[Two, R]

def twice[R](x: RMul2[R]) -> R:
    return 2 * x

但是,对于实现 __add__ 但不实现 __radd__ 的类型怎么办?在这种情况下,我们可以返回 x * 2 作为后备方案(假设交换律)。因为 optype.Can* 协议是运行时可检查的,所以修订后的 twice2 函数可以紧凑地写成

Python 3.10 Python 3.12+
from optype import CanMul

Mul2: TypeAlias = CanMul[Two, R]
CMul2: TypeAlias = Mul2[R] | RMul2[R]

def twice2(x: CMul2[R]) -> R:
    if isinstance(x, CanRMul):
        return 2 * x
    else:
        return x * 2
from optype import CanMul

type Mul2[R] = CanMul[Two, R]
type CMul2[R] = Mul2[R] | RMul2[R]

def twice2[R](x: CMul2[R]) -> R:
    if isinstance(x, CanRMul):
        return 2 * x
    else:
        return x * 2

请参阅 examples/twice.py 以获取完整示例。

参考

optype 的 API 是平的;只需要一个 import optype as opt 就可以了(除了 optype.numpy)。

optype

optype 中有四种类型的事物存在

  • optype.Can{} 类型描述了可以对其执行的操作。例如,任何 CanAbs[T] 类型都可以用作具有返回类型 T 的内置函数 abs() 的参数。大多数 Can{} 实现了一个特殊方法,其名称直接与类型匹配。例如,CanAbs 实现了 __abs__CanAdd 实现了 __add__ 等。
  • optype.Has{}Can{} 的类似物,但用于特殊属性。例如,HasName 有一个 __name__ 属性,HasDict 有一个 __dict__ 属性等。
  • optype.Does{} 描述了操作符的类型。因此,DoesAbs 是内置函数 abs({}) 的类型,而 DoesPos 是前缀操作符 +{} 的类型。
  • optype.do_{}Does{} 的正确类型实现。对于每个 do_{},都有一个 Does{},反之亦然。因此,do_abs: DoesAbsabs({}) 的类型别名,而 do_pos: DoesPosoperator.pos 的类型化版本。与 operators 相比,optype.do_ 操作符更完整,具有可运行时访问的类型注释,并且有你不必记住的名字。

参考文档的结构如下

这里所有 typing 协议 都位于根 optype 命名空间中。它们是 运行时可检查的,因此你可以执行例如 isinstance('snail', optype.CanAdd),如果你想要检查 snail 是否实现了 __add__

collections.abc不同,optype的协议不是抽象基类,也就是说它们不扩展abc.ABC,仅扩展typing.Protocol。这允许optype协议作为.pyi类型存根的构建块。

内置类型转换

这些特殊方法的返回类型是不变的。如果返回其他(子)类型,Python将引发错误。这就是为什么这些optype接口不接受泛型类型参数。

operator 操作数
表达式 函数 类型 方法 类型
complex(_) do_complex DoesComplex __complex__ CanComplex
float(_) do_float DoesFloat __float__ CanFloat
int(_) do_int DoesInt __int__ CanInt[R: int = int]
bool(_) do_bool DoesBool __bool__ CanBool[R: bool = bool]
bytes(_) do_bytes DoesBytes __bytes__ CanBytes[R: bytes = bytes]
str(_) do_str DoesStr __str__ CanStr[R: str = str]

[!NOTE] 可以用作typing.Literal的类型,其Can*接口接受一个可选的类型参数R。这可以用来指明字面量返回类型,以进行精确的打字,例如NoneTrue42分别是CanBool[Literal[False]]CanInt[Literal[1]]CanStr[Literal['42']]的实例。

这些格式化方法可以返回子类型为内置的str的实例。同样适用于__format__参数。所以如果你是一个想黑入Python的f-strings的10x开发者,但只有当你类型提示准确无误时;optype是你的朋友。

operator 操作数
表达式 函数 类型 方法 类型
repr(_) do_repr DoesRepr __repr__ CanRepr[R: str = str]
format(_, x) do_format DoesFormat __format__ CanFormat[T: str = str, R: str = str]

此外,optype为具有(自定义)哈希索引方法的类型提供了协议

operator 操作数
表达式 函数 类型 方法 类型
hash(_) do_hash DoesHash __hash__ CanHash
_.__index__() (文档) do_index DoesIndex __index__ CanIndex[R: int = int]

丰富关系

“丰富”的比较特殊方法通常返回一个bool。然而,可以返回任何类型的实例(例如numpy数组)。这就是为什么相应的optype.Can*接口接受一个返回类型的第二个类型参数,当省略时默认为bool。第一个类型参数与传递的方法参数匹配,即右侧操作数,在此表示为x

operator 操作数
表达式 reflected 函数 类型 方法 类型
_ == x x == _ do_eq DoesEq __eq__ CanEq[T = object, R = bool]
_ != x x != _ do_ne DoesNe __ne__ CanNe[T = object, R = bool]
_ < x x > _ do_lt DoesLt __lt__ CanLt[T, R = bool]
_ <= x x >= _ do_le DoesLe __le__ CanLe[T, R = bool]
_ > x x < _ do_gt DoesGt __gt__ CanGt[T, R = bool]
_ >= x x <= _ do_ge DoesGe __ge__ CanGe[T, R = bool]

二元运算

Python文档中,这些被称为“算术运算”。但是操作数并不限于数值类型,并且由于这些操作不是必需的交换律,可能是非确定性的,并且可能有副作用。将它们归类为“算术”至少是一个小小的夸张。

operator 操作数
表达式 函数 类型 方法 类型
_ + x do_add DoesAdd __add__ CanAdd[T, R]
_ - x do_sub DoesSub __sub__ CanSub[T, R]
下划线 * x do_mul DoesMul __mul__ CanMul[T, R]
下划线 @ x do_matmul DoesMatmul __matmul__ CanMatmul[T, R]
下划线 / x do_truediv DoesTruediv __truediv__ CanTruediv[T, R]
下划线 // x do_floordiv DoesFloordiv __floordiv__ CanFloordiv[T, R]
下划线 % x do_mod DoesMod __mod__ CanMod[T, R]
divmod(_, x) do_divmod DoesDivmod __divmod__ CanDivmod[T, R]
下划线 ** x
pow(_, x)
do_pow/2 DoesPow __pow__ CanPow2[T, R]
CanPow[T, None, R, Never]
pow(_, x, m) do_pow/3 DoesPow __pow__ CanPow3[T, M, R]
CanPow[T, M, Never, R]
下划线 << x do_lshift DoesLshift __lshift__ CanLshift[T, R]
下划线 >> x do_rshift DoesRshift __rshift__ CanRshift[T, R]
下划线 & x do_and DoesAnd __and__ CanAnd[T, R]
下划线 ^ x do_xor DoesXor __xor__ CanXor[T, R]
下划线 | x do_or DoesOr __or__ CanOr[T, R]

![NOTE] 因为 pow() 可以接受一个可选的第三个参数,optype 为具有两个和三个参数的 pow() 提供了单独的接口。此外,还有重载的交集类型 CanPow[T, M, R, RM] =: CanPow2[T, R] & CanPow3[T, M, RM],作为可以接受可选第三个参数的类型的接口。

反射操作

对于上面的二元前缀运算符,optype 还提供了具有 反射(交换)操作数的接口,例如 __radd____add__ 的反射。它们的命名与原始的相同,但前面有 CanR 前缀,即 __name__.replace('Can', 'CanR')

operator 操作数
表达式 函数 类型 方法 类型
x + _ do_radd DoesRAdd __radd__ CanRAdd[T, R]
x - _ do_rsub DoesRSub __rsub__ CanRSub[T, R]
x * _ do_rmul DoesRMul __rmul__ CanRMul[T, R]
x @ _ do_rmatmul DoesRMatmul __rmatmul__ CanRMatmul[T, R]
x / _ do_rtruediv DoesRTruediv __rtruediv__ CanRTruediv[T, R]
x // _ do_rfloordiv DoesRFloordiv __rfloordiv__ CanRFloordiv[T, R]
x % _ do_rmod DoesRMod __rmod__ CanRMod[T, R]
divmod(x, _) do_rdivmod DoesRDivmod __rdivmod__ CanRDivmod[T, R]
x ** _
pow(x, _)
do_rpow DoesRPow __rpow__ CanRPow[T, R]
x << _ do_rlshift DoesRLshift __rlshift__ CanRLshift[T, R]
x >> _ do_rrshift DoesRRshift __rrshift__ CanRRshift[T, R]
x & _ do_rand DoesRAnd __rand__ CanRAnd[T, R]
x ^ _ do_rxor DoesRXor __rxor__ CanRXor[T, R]
x | _ do_ror DoesROr __ror__ CanROr[T, R]

![NOTE] CanRPow 对应于 CanPow2;3参数的“模” pow 在 Python 中不会反射。

根据相关的 python 文档

注意三参数的 pow() 不会尝试调用 __rpow__()(强制转换规则将变得过于复杂)。

就地操作

类似于反射操作,原地/增强操作前缀为 CanI,即

operator 操作数
表达式 函数 类型 方法 类型
下划线 += x do_iadd DoesIAdd __iadd__ CanIAdd[T, R]
CanIAddSelf[T]
下划线 -= x do_isub DoesISub __isub__ CanISub[T, R]
CanISubSelf[T]
下划线 *= x do_imul DoesIMul __imul__ CanIMul[T, R]
CanIMulSelf[T]
下划线 @= x do_imatmul 是否支持IMatmul __imatmul__ 是否支持IMatmul[T, R]
是否支持IMatmulSelf[T]
_ /= x 执行do_itruediv 是否支持ITruediv __itruediv__ 是否支持ITruediv[T, R]
是否支持ITruedivSelf[T]
_ //= x 执行do_ifloordiv 是否支持IFloordiv __ifloordiv__ 是否支持IFloordiv[T, R]
是否支持IFloordivSelf[T]
_ %= x 执行do_imod 是否支持IMod __imod__ 是否支持IMod[T, R]
是否支持IModSelf[T]
_ **= x 执行do_ipow 是否支持IPow __ipow__ 是否支持IPow[T, R]
是否支持IPowSelf[T]
_ <<= x 执行do_ilshift 是否支持ILshift __ilshift__ 是否支持ILshift[T, R]
是否支持ILshiftSelf[T]
_ >>= x 执行do_irshift 是否支持IRshift __irshift__ 是否支持IRshift[T, R]
是否支持IRshiftSelf[T]
_ &= x 执行do_iand 是否支持IAnd __iand__ 是否支持IAnd[T, R]
是否支持IAndSelf[T]
_ ^= x 执行do_ixor 是否支持IXor __ixor__ 是否支持IXor[T, R]
是否支持IXorSelf[T]
_ |= x 执行do_ior 是否支持IOr __ior__ 是否支持IOr[T, R]
是否支持IOrSelf[T]

这些原地操作符通常在执行一些原地修改后返回自身。但不幸的是,目前无法使用Self来实现这一点(即不允许像type MyAlias[T] = optype.CanIAdd[T, Self]这样的用法)。因此,为了减轻这种难以忍受的痛苦,optype为您提供了现成的别名。它们的名称相同,但额外添加了*Self后缀,例如optype.CanIAddSelf[T]

一元运算

operator 操作数
表达式 函数 类型 方法 类型
+_ 执行do_pos 是否支持Pos __pos__ 是否支持Pos[R]
是否支持PosSelf
-_ 执行do_neg 是否支持Neg __neg__ 是否支持Neg[R]
是否支持NegSelf
~_ 执行do_invert 是否支持Invert __invert__ 是否支持Invert[R]
是否支持InvertSelf
abs(_) 执行do_abs 是否支持Abs __abs__ 是否支持Abs[R]
是否支持AbsSelf

舍入

round()内置函数接受一个可选的第二个参数。从类型的角度来看,round()有两个重载版本,一个参数为一个,另一个参数为两个。对于这两个重载版本,optype提供了独立的操作数接口:CanRound1[R]CanRound2[T, RT]。此外,optype还提供了它们的(重载的)交集类型:CanRound[T, R, RT] = CanRound1[R] & CanRound2[T, RT]

operator 操作数
表达式 函数 类型 方法 类型
round(_) 执行do_round/1 是否支持Round __round__/1 CanRound1[T = int]
round(_, n) 执行do_round/2 是否支持Round __round__/2 CanRound2[T = int, RT = float]
round(_, n=...) 执行do_round 是否支持Round __round__ CanRound[T = int, R = int, RT = float]

例如,类型检查器会将以下代码标记为有效(在严格模式下使用pyright进行测试)

x: float = 3.14
x1: CanRound1[int] = x
x2: CanRound2[int, float] = x
x3: CanRound[int, int, float] = x

此外,还有来自math标准库的替代舍入函数

operator 操作数
表达式 函数 类型 方法 类型
math.trunc(_) 执行do_trunc 是否支持Trunc __trunc__ CanTrunc[R = int]
math.floor(_) 执行do_floor 是否支持Floor __floor__ CanFloor[R = int]
math.ceil(_) 执行do_ceil 是否支持Ceil __ceil__ CanCeil[R = int]

几乎所有实现都使用int作为R。实际上,如果没有指定R的类型,它将默认为int。但从技术上讲,这些方法可以返回任何内容。

可调用对象

operator不同,optype为可调用对象提供了操作符:optype.do_call(f, *args. **kwargs)

CanCall类似于collections.abc.Callable,但它可以在运行时进行检查,并且不使用晦涩的技巧。

operator 操作数
表达式 函数 类型 方法 类型
_(*args, **kwargs) 执行do_call 是否支持Call __call__ CanCall[**Pss, R]

[!注意] Pyright(以及可能的其他类型检查器)通常比 optype.CanCall 在更多的地方接受 collections.abc.Callable。这可能与其对 typing.ParamSpec 的协/逆变指定不足有关(它们几乎总是应该是逆变的,但当前它们只能是不变的)。

如果你遇到这种情况,请就此事提交一个问题,以便我们进一步调查。

迭代

Python 中 iter(_) 的操作数 x 被称为 可迭代对象,这是 collections.abc.Iterable[V] 常用的用途(例如作为基类,或用于实例检查)。

optype 类似物是 CanIter[R],正如其名称所示,它也实现了 __iter__。但与 Iterable[V] 不同,其类型参数 R 绑定到 iter(_) -> R 的返回类型。这使得能够注释 iter(_) 返回的 可迭代对象 的具体类型。仅能注释迭代值的类型。Iterable[V]。要了解为什么这不可能,请参阅python/typing#548

collections.abc.Iterator[V] 更加奇怪;它是 Iterable[V] 的子类型。对于那些熟悉 collections.abc 的人来说,这可能是个惊喜,但迭代器只需要实现 __next__,不需要 __iter__。这意味着 Iterator[V] 是不必要地限制性的。除了在理论上是“丑陋的”之外,它还有重大的性能影响,因为 isinstancetyping.Protocol 上的时间复杂度是 $O(n)$,其中 $n$ 指的是成员的数量。所以即使忽略继承和 abc.ABC 的使用开销,collections.abc.Iterator 仍然比它需要的慢两倍。

这就是为什么 optype.CanNext[V]optype.CanNext[V] 是比 abracadabra 集合中的 IterableIterator 更好的替代品的原因之一。它们的定义如下

operator 操作数
表达式 函数 类型 方法 类型
next(_) do_next DoesNext __next__ CanNext[V]
iter(_) do_iter DoesIter __iter__ CanIter[R: CanNext[object]]

为了与 collections.abc 兼容,存在 optype.CanIterSelf[V],这是一个协议,其 __iter__ 返回 typing.Self,以及一个返回 T__next__ 方法。即它等同于 collections.abc.Iterator[V],但没有 abc 的无聊之处。

等待对象

optype 几乎与 collections.abc.Awaitable[R] 相同,只是 optype.CanAwait[R] 是一个纯接口,而 Awaitable 也是一个抽象基类(这使得在编写存根时完全无用地存在)。

operator 操作数
表达式 方法 类型
await _ __await__ CanAwait[R]

异步迭代

是的,你猜对了;abracadabra 集合为异步可迭代对象(或者说是“iteramblers”?)犯了完全相同的错误。

但别担心;这里就是 optype 的替代品

operator 操作数
表达式 函数 类型 方法 类型
anext(_) do_anext DoesANext __anext__ CanANext[V]
aiter(_) do_aiter DoesAIter __aiter__ CanAIter[R: CanAnext[object]]

但是等等,V 不应该是 CanAwait 吗?好吧,只有当你不想被炒鱿鱼时……从技术上来说,__anext__ 可以返回任何类型,而 anext 会传递它而不纠缠(实例检查很慢,现在别再烦我了)。有关详细信息,请参阅python/typeshed#7491。尽管某事是合法的,但这并不意味着它是个好主意(别吃黄色的雪)。

此外,还有 optype.CanAIterSelf[R],它同时具有 __aiter__() -> Self__anext__() -> V 方法。

容器

operator 操作数
表达式 函数 类型 方法 类型
len(_) do_len DoesLen __len__ CanLen[R: int = int]
_.__length_hint__() (文档) do_length_hint DoesLengthHint __length_hint__ CanLengthHint[R: int = int]
_[k] do_getitem 支持Getitem __getitem__ 支持Getitem[K, V]
_.__missing__() (文档) do_missing 支持Missing __missing__ 支持Missing[K, D]
_[k] = v do_setitem 支持Setitem __setitem__ 支持Setitem[K, V]
del _[k] do_delitem 支持Delitem __delitem__ 支持Delitem[K]
k in _ do_contains 支持Contains __contains__ 支持Contains[K = object]
reversed(_) do_reversed 支持Reversed __reversed__ 支持Reversed[R],或者
支持Sequence[I, V, N = int]

由于支持Missing[K, D]通常在没有支持Getitem[K, V]的情况下不会显示,因此optype方便地将它们组合为optype.CanGetMissing[K, V, D=V]

类似地,存在optype.CanSequence[K: CanIndex | slice, V],这是支持Len支持Item[I, V]的组合,充当更具体和灵活的collections.abc.Sequence[V]

属性

operator 操作数
表达式 函数 类型 方法 类型
v = _.k或者
v = getattr(_, k)
do_getattr 支持Getattr __getattr__ 支持Getattr[K: str = str, V = object]
_.k = v或者
setattr(_, k, v)
do_setattr 支持Setattr __setattr__ 支持Setattr[K: str = str, V = object]
del _.k或者
delattr(_, k)
do_delattr 支持Delattr __delattr__ 支持Delattr[K: str = str]
dir(_) do_dir 支持Dir __dir__ 支持Dir[R: CanIter[CanIterSelf[str]]]

上下文管理器

支持with语句。

operator 操作数
表达式 方法 类型
__enter__ 支持Enter[C],或者支持EnterSelf
__exit__ 支持Exit[R = None]
with _ as c __enter__,以及
__exit__
支持With[C, R=None],或者
支持WithSelf[R=None]

支持EnterSelf支持WithSelf分别是支持Enter[Self]支持With[Self, R]的(运行时可检查)别名。

对于async with语句,接口看起来非常相似

operator 操作数
表达式 方法 类型
__aenter__ 支持AEnter[C],或者
支持AEnterSelf
__aexit__ 支持AExit[R=None]
async with _ as c __aenter__,以及
__aexit__
支持AsyncWith[C, R=None],或者
支持AsyncWithSelf[R=None]

描述符

描述符的接口。

operator 操作数
表达式 方法 类型
v: V = T().d
vt: VT = T.d
__get__ 支持Get[T: object, V, VT = V]
T().k = v __set__ 支持Set[T: object, V]
del T().k __delete__ 支持Delete[T: object]
class T: d = _ __set_name__ 支持SetName[T: object, N: str = str]

缓冲区类型

使用缓冲协议模拟缓冲类型接口。

operator 操作数
表达式 方法 类型
v = memoryview(_) __buffer__ 支持Buffer[T: int = int]
del v __release_buffer__ 支持ReleaseBuffer

optype.copy

对于标准库copyoptype.copy提供以下运行时检查接口

copy标准库 optype.copy
函数 类型 方法
copy.copy(_) -> R __copy__() -> R 支持Copy[R]
copy.deepcopy(_, memo={}) -> R __deepcopy__(memo, /) -> R 支持Deepcopy[R]
copy.replace(_, /, **changes: V) -> R [1] __replace__(**changes: V) -> R 支持Replace[V, R]

[1] copy.replace需要python>=3.13(但optype.copy.CanReplace不需要)

在实际应用中,实例的副本与原始实例相同类型是有意义的。但由于 typing.Self 不能用作类型参数,这导致难以正确地定义类型。相反,您可以使用 optype.copy.Can{}Self 类型,这些类型是以下(递归)类型别名的运行时检查等价物。

type CanCopySelf = CanCopy[CanCopySelf]
type CanDeepcopySelf = CanDeepcopy[CanDeepcopySelf]
type CanReplaceSelf[V] = CanReplace[V, CanReplaceSelf[V]]

optype.dataclasses

对于标准库中的 dataclassesoptype.dataclasses 提供了 HasDataclassFields[V: Mapping[str, Field]] 接口。它可以方便地用来检查一个类型或实例是否为数据类,即 isinstance(obj, HasDataclassFields)

optype.inspect

一组用于运行时检查类型、模块和其他对象的函数。

函数 描述
get_args(_)

typing.get_args() 的一个更好的替代品,它可以

  • 解包 typing.Annotated 和 Python 3.12 的 type _ 别名类型(即 typing.TypeAliasType),
  • 递归地展开联合和嵌套的 typing.Literal 类型,
  • 如果不是一个类型表达式则抛出 TypeError

返回一个类型参数或参数的 tuple[type | object, ...]

为了说明 typing.get_args(许多)问题之一

>>> from typing import Literal, TypeAlias, get_args
>>> Falsy: TypeAlias = Literal[None] | Literal[False, 0] | Literal['', b'']
>>> get_args(Falsy)
(typing.Literal[None], typing.Literal[False, 0], typing.Literal['', b''])

但这与 官方 typing 文档 直接矛盾

当 Literals 被多个值参数化时,它被视为与这些类型的联合完全等价。也就是说,Literal[v1, v2, v3] 等价于 Literal[v1] | Literal[v2] | Literal[v3]

因此,应该使用 optype.inspect.get_args

>>> import optype as opt
>>> opt.inspect.get_args(Falsy)
(None, False, 0, '', b'')

typing.get_args 的另一个问题是与 Python 3.12 的 type _ = ... 别名相关,这些别名旨在替代 _: typing.TypeAlias = ...,因此应该同等对待

>>> import typing
>>> import optype as opt
>>> type StringLike = str | bytes
>>> typing.get_args(StringLike)
()
>>> opt.inspect.get_args(StringLike)
(<class 'str'>, <class 'bytes'>)

很明显,typing.get_args 在这里表现得非常糟糕;如果它会引发错误会更好,但它反而返回一个空元组,隐藏了它不支持新的 type _ = ... 别名的事实。但幸运的是,optype.inspect.get_args 没有这个问题,它像对待 typing.Alias 一样对待它(其他 optype.inspect 函数也是如此)。

get_protocol_members(_)

typing.get_protocol_members() 的一个更好的替代品,它

  • 不需要 Python 3.13 或更高版本,
  • 支持 Python 3.12 及以上版本的 PEP 695 type _ 别名类型,
  • 解包 typing.Literal 的联合...
  • ...并在另一个 typing.Literal 内嵌套时展开它们,
  • typing.Annotated[T] 视为 T
  • 如果传入的值不是一个类型表达式则抛出 TypeError

返回一个包含成员名称的 frozenset[str]

get_protocols(_)

返回一个包含传递的模块中公共协议的 frozenset[type]。传递 private=True 以返回私有协议。

is_iterable(_)

检查对象是否可以迭代,即它是否可以用于 for 循环,而不尝试这样做。如果返回 True,则对象是 optype.typing.AnyIterable 实例。

is_final(_)

检查类型、方法 / 类方法 / 静态方法 / 属性是否被装饰为 @typing.final

请注意,除非将 @final 装饰器放在 @property 装饰器下方,否则不会识别 @property。有关更多信息,请参阅函数文档字符串。

is_protocol(_)

Python 3.13 中新增的 typing.is_protocol 的后向移植,相当于 typing_extensions.is_protocol 的重新导出。

is_runtime_protocol(_)

检查类型表达式是否为 运行时协议,即带有 @typing.runtime_checkable 装饰的 typing.Protocol 类型(也支持 typing_extensions)。

is_union_type(_)

检查类型是否为 typing.Union 类型,例如 str | int

isinstance(_, types.Union) 不同,此函数对于用户定义的 GenericProtocol 类型的联合也会返回 True(因为这些可能是不同类型的联合)。

is_generic_alias(_)

检查类型是否为 索引 类型,例如 list[str]optype.CanNext[int],但不包括 listCanNext

isinstance(_, typing.GenericAlias) 不同,此函数对于用户定义的 GenericProtocol 类型也会返回 True(因为这些类型使用了不同的泛型别名)。

虽然技术上 T1 | T2 被表示为 typing.Union[T1, T2](这是一个(特殊的)泛型别名),但 is_generic_alias 对于此类联合类型会返回 False,因为将 T1 | T2 称为索引类型在实际上没有太多意义。

[!NOTE] 所有 optype.inspect 中的函数也适用于 Python 3.12 的 type _ 别名(即 types.TypeAliasType)以及 typing.Annotated

optype.json

json 标准库的类型别名

Value AnyValue
json.load(s) 返回类型 json.dumps(s) 输入类型
Array[V: Value = Value] AnyArray[V: AnyValue = AnyValue]
Object[V: Value = Value] AnyObject[V: AnyValue = AnyValue]

(Any)Value 可以是任何 JSON 输入,即 Value | Array | Object 等同于 Value。还值得注意的是,ValueAnyValue 的子类型,这意味着 AnyValue | Value 等同于 AnyValue

optype.pickle

对于 pickle 标准库,optype.pickle 提供以下接口

方法 signature (bound) 类型
__reduce__ () -> R CanReduce[R: str | tuple = ...]
__reduce_ex__ (CanIndex) -> R CanReduceEx[R: str | tuple = ...]
__getstate__ () -> S CanGetstate[S]
__setstate__ (S) -> None CanSetstate[S]
__getnewargs__
__new__
() -> tuple[V, ...]
(V) -> Self
CanGetnewargs[V]
__getnewargs_ex__
__new__
() -> tuple[tuple[V, ...], dict[str, KV]]
(*tuple[V, ...], **dict[str, KV]) -> Self
CanGetnewargsEx[V, KV]

optype.string

string 标准库包含实用的常量,但它有两个问题

  • 常量包含一组字符,但表示为单个字符串。这使得为单个字符进行类型提示实际上变得不可能,因此 typeshed 目前将这些常量类型化为 LiteralString
  • 常量名称不统一,且不符合 PEP 8

因此,optype.string 提供了一个替代接口,与 string 兼容,但略有不同

  • 对于每个常量,都有一个对应的 Literal 类型别名用于表示单个字符。其名称与常量名称相同,但使用单数形式而不是复数形式。
  • 与单个字符串不同,optype.string 使用字符的 tuple,这样每个字符都有自己的 typing.Literal 注解。请注意,这仅在 (based)pyright / pylance 中进行了测试,因此可能不与 mypy 一起使用(它比代码行数有更多的错误)。
  • 常量的名称与 PEP 8 一致,并使用后缀表示变体,例如 DIGITS_HEX 而不是 hexdigits
  • string 不同,optype.string 为二进制数字 '0''1' 有一个常量(和类型别名)DIGITS_BIN(和 DigitBin)。因为除了 builtins 中的 octhex 函数之外,还有 builtins.bin 函数。
string._ optype.string._
常量 字符类型 常量 字符类型
缺失 DIGITS_BIN DigitBin
octdigits LiteralString DIGITS_OCT DigitOct
digits DIGITS Digit
hexdigits DIGITS_HEX DigitHex
ascii_letters LETTERS Letter
ascii_lowercase LETTERS_LOWER LetterLower
ascii_uppercase LETTERS_UPPER LetterUpper
punctuation PUNCTUATION Punctuation
whitespace WHITESPACE Whitespace
printable PRINTABLE Printable

每个 optype.string 常量都与相应的 string 常量完全相同(在连接/拆分之后),例如

>>> import string
>>> import optype as opt
>>> ''.join(opt.string.PRINTABLE) == string.printable
True
>>> tuple(string.printable) == opt.string.PRINTABLE
True

同样,常量 Literal 类型中的值与常量的值完全匹配

>>> import optype as opt
>>> from optype.inspect import get_args
>>> get_args(opt.string.Printable) == opt.string.PRINTABLE
True

optype.inspect.get_argstyping.get_args 的一个无故障变体,它正确地扁平化嵌套字面量、类型联合和 PEP 695 类型别名,以匹配官方类型规范。换句话说,typing.get_args 是另一个基本有缺陷的 python-typing 功能,在您最需要它的情况下毫无用处。

optype.typing

Any* 类型别名

为可以始终传递给 intfloatcomplexitertyping.Literal 的任何内容提供类型别名

Python 构造函数 optype.typing 别名
int(_) AnyInt
float(_) AnyFloat
complex(_) AnyComplex
iter(_) AnyIterable
typing.Literal[_] AnyLiteral

[!NOTE] 即使 某些 strbytes 可以转换为 intfloatcomplex,但大多数不能,因此不包括在这些类型别名中。

Empty* 类型别名

这些是空内置类型或集合,即长度为 0 或不产生元素的集合。

实例 optype.typing 类型
'' EmptyString
b'' EmptyBytes
() EmptyTuple
[] EmptyList
{} EmptyDict
set() EmptySet
(i for i in range(0)) EmptyIterable

文字类型

字面量值 optype.typing 类型 注释
{False, True} LiteralFalse 类似于 typing.LiteralString,但用于 bool
{0, 1, ..., 255} LiteralByte 范围在 0-255 之间的整数,组成 bytesbytearray 对象。

optype.numpy

Optype 支持 NumPy 1 和 2。当前支持的最小版本是 1.24,遵循 NEP 29SPEC 0

当使用 optype.numpy 时,建议使用带有 numpy 附加组件安装 optype,以确保版本兼容性

pip install "optype[numpy]"

[!NOTE] 对于 optype.numpy 文档的其余部分,假设以下导入别名可用。

from typing import Any, Literal
import numpy as np
import numpy.typing as npt
import optype.numpy as onp

为了简洁和可读性,将使用自 Python 3.13 以来支持的 PEP 695PEP 696 类型参数语法。

数组

Optype为np.ndarray提供了通用的onp.Array类型别名。它类似于npt.NDArray,但包含两个(可选)类型参数:一个匹配形状类型ND: tuple[int, ...]),另一个匹配标量类型ST: np.generic)。

npt.NDArrayonp.Array的定义并排放置时,它们之间的差异变得明显。

numpy.typing.NDArray optype.numpy.Array
type NDArray[
    # no shape type
    ST: np.generic,  # no default
] = np.ndarray[Any, np.dtype[ST]]
type Array[
    ND: tuple[int, ...] = tuple[int, ...],
    ST: np.generic = np.generic,
] = np.ndarray[ND, np.dtype[ST]]

[!IMPORTANT] 目前,np.ndarray的形状类型参数(ND)被定义为不可变的。这是不正确的:它应该是协变的。

这意味着ND: tuple[int, ...]onp.Array中也是不可变的。

结果是,例如,def span(a: onp.Array[tuple[int], ST])将不接受onp.Array[tuple[Literal[42]]]作为参数,尽管Literal[42]int的子类型。

有关详细信息,请参阅numpy/numpy#25729numpy/numpy#26081

实际上,`onp.Array`几乎是`npt.NDArray`的泛化。

这是因为可以用onp.Array完全定义npt.NDArray(但反之则不行)。

type NDArray[ST: np.generic] = onp.Array[Any, ST]

有了onp.Array,就可以对数组形状进行类型注解。

[!NOTE] 据消息人士透露,onp.Array可能会在不久的将来被回滚到NumPy中。

UFunc

Numpy的大部分公共API由通用函数组成,通常表示为ufuncs,它们是np.ufunc的(可调用)实例。

[!TIP] 可以使用np.frompyfunc创建自定义ufuncs,也可以通过实现所需属性和方法(即鸭子类型)的用户定义类来创建。

但是,np.ufunc有一个大问题;它不接受类型参数。这使得正确注释其可调用签名及其文字属性(例如.nin.identity)变得非常困难。

这就是optype.numpy.UFunc发挥作用的地方:它是一个运行时可检查的泛型类型协议,已经经过彻底的类型和单元测试,以确保与NumPy的所有ufunc定义兼容。它的泛型类型签名大致如下

UFunc[
    # The type of the (bound) `__call__` method.
    Fn: CanCall = CanCall,
    # The types of the `nin` and `nout` (readonly) attributes.
    # Within numpy these match either `Literal[1]` or `Literal[2]`.
    Nin: int = int,
    Nout: int = int,
    # The type of the `signature` (readonly) attribute;
    # Must be `None` unless this is a generalized ufunc (gufunc), e.g.
    # `np.matmul`.
    Sig: str | None = str | None,
    # The type of the `identity` (readonly) attribute (used in `.reduce`).
    # Unless `Nin: Literal[2]`, `Nout: Literal[1]`, and `Sig: None`,
    # this should always be `None`.
    # Note that `complex` also includes `bool | int | float`.
    Id: complex | bytes | str | None = float | None,
]

[!NOTE] 不幸的是,np.ufunc的额外可调用方法(atreducereduceataccumulateouter)的注解错误(作为None 属性,尽管在运行时它们是调用时会引发ValueError的方法)。这目前使得在optype.numpy.UFunc中正确注解这些方法变得不可能;这样做将使其与NumPy的ufuncs不兼容。

形状类型别名

形状不过是(非负)整数的一个元组,即tuple[int, ...]的一个实例,例如(42,)(6, 6, 6)()。形状的长度通常被称为数组的维度数维度。对于数组,这可以通过np.ndarray.ndim访问,它是len(np.ndarray.shape)的一个别名。

[!NOTE] 在NumPy 2之前,最大维度数是32,但后来已增加到64。

为了使对数组形状进行类型注解更容易,optype提供了两个形状类型别名家族:AtLeast{N}DAtMost{N}D{N}应由维度数替换,目前限制为0123

这两个家族都是泛型的,它们的(可选)类型参数必须是int(默认)或文字(非负)整数,即类似于typing.Literal[N: int]

[!NOTE] NumPy具有shape参数的函数通常也接受一个“基础”int,它是tuple[int]的缩写。但为了保持一致性,AtLeast{N}DAtMost{N}D仅限于整数元组

名称 AtLeast{N}DAtMost{N}D 非常直观易懂

  • AtLeast{N}D 是一个 tuple[int, ...],其 ndim >= N
  • AtMost{N}D 是一个 tuple[int, ...],其 ndim <= N

形状别名的大致定义如下

AtLeast{N}D AtMost{N}D
类型签名 别名类型 类型签名 类型别名
type AtLeast0D[
    Ds: int = int,
]
tuple[Ds, ...]
type AtMost0D
tuple[()]
type AtLeast1D[
    D0: int = int,
    Ds: int = int,
]
tuple[
    D0,
    *tuple[Ds, ...],
]
type AtMost1D[
    D0: int = int,
]
tuple[D0] | AtMost0D
type AtLeast2D[
    D0: int = int,
    D1: int = int,
    Ds: int = int,
]
tuple[
    D0,
    D1,
    *tuple[Ds, ...],
]
type AtMost2D[
    D0: int = int,
    D1: int = int,
]
(
    tuple[D0, D1]
    | AtMost1D[D0]
)
type AtLeast3D[
    D0: int = int,
    D1: int = int,
    D2: int = int,
    Ds: int = int,
]
tuple[
    D0,
    D1,
    D2,
    *tuple[Ds, ...],
]
type AtMost3D[
    D0: int = int,
    D1: int = int,
    D2: int = int,
]
(
    tuple[D0, D1, D2]
    | AtMost2D[D0, D1]
)

标量

optype.numpy.Scalar 接口是一个通用的运行时可检查协议,它可以从名称和类型的角度看作是“更具体”的 np.generic

它的类型签名大致如下

Scalar[
    # The "Python type", so that `Scalar.item() -> PT`.
    PT: object,
    # The "N-bits" type (without having to deal with `npt.NBitBase`).
    # It matches the `itemsize: NB` property.
    NB: int = int,
]

它可以被用作,例如

are_birds_real: Scalar[bool, Literal[1]] = np.bool_(True)
the_answer: Scalar[int, Literal[2]] = np.uint16(42)
alpha: Scalar[float, Literal[8]] = np.float64(1 / 137)

[!NOTE] 对于 itemsize 的第二个类型参数可以省略,这等同于将其设置为 int,因此 Scalar[PT]Scalar[PT, int] 是等效的。

DType

在 NumPy 中,一个 dtype(数据类型)对象是 numpy.dtype[ST: np.generic] 类型的实例。它通常用于传递标量的元数据,例如在数组中。

由于 np.dtype 的类型参数是可选的,因此使用别名 optype.numpy.DType 可能更方便,它定义为

type DType[ST: np.generic = np.generic] = np.dtype[ST]

除了“驼峰命名法”的名称外,与 np.dtype 的唯一区别是类型参数可以省略,在这种情况下,它等同于 np.dtype[np.generic],但更简洁。

Any*ArrayAny*DType

Any{Scalar}Array 类型别名描述了当传递给 numpy.asarray(或任何其他 numpy.ndarray 构造函数)时,结果为具有特定 dtype(即 numpy.dtypes.{Scalar}DType)的所有内容。

[!NOTE] numpy.dtypes 文档 自 NumPy 1.25 以来存在,但其类型注解在 NumPy 2.1 之前是错误的(参见 numpy/numpy#27008

有关 NumPy 标量类型层次结构的更多信息,请参阅 文档

抽象类型
numpy._ optype.numpy._
标量类型 基本类型 数组类型 dtype 类型
通用类型 AnyArray AnyDType
数字 通用类型 AnyNumberArray AnyNumberDType
整数 数字 AnyIntegerArray AnyIntegerDType
不精确数字 AnyInexactArray AnyInexactDType
无符号整数 整数 AnyUnsignedIntegerArray AnyUnsignedIntegerDType
有符号整数 AnySignedIntegerArray AnySignedIntegerDType
浮点数 不精确数字 AnyFloatingArray AnyFloatingDType
复浮点数 AnyComplexFloatingArray AnyComplexFloatingDType
无符号整数
numpy._ optype.numpy._
标量类型 基本类型 数组类型 dtype 类型
uint8 无符号整数 AnyUInt8Array AnyUInt8DType
uint16 AnyUInt16Array AnyUInt16DType
uint32 AnyUInt32Array AnyUInt32DType
uint64 AnyUInt64Array AnyUInt64DType
uintp AnyUIntPArray AnyUIntPDType
ubyte AnyUByteArray AnyUByteDType
ushort AnyUShortArray AnyUShortDType
uintc AnyUIntCArray AnyUIntCDType
ulong AnyULongArray AnyULongDType
ulonglong AnyULongLongArray AnyULongLongDType
有符号整数
numpy._ optype.numpy._
标量类型 基本类型 数组类型 dtype 类型
int8 有符号整数 AnyInt8Array AnyInt8DType
int16 AnyInt16Array AnyInt16DType
int32 AnyInt32Array AnyInt32DType
int64 AnyInt64Array AnyInt64DType
intp AnyIntPArray AnyIntPDType
byte AnyByteArray AnyByteDType
short AnyShortArray AnyShortDType
intc AnyIntCArray AnyIntCDType
long AnyLongArray AnyLongDType
longlong AnyLongLongArray AnyLongLongDType
浮点数
numpy._ optype.numpy._
标量类型 基本类型 数组类型 dtype 类型
float16 浮点数 AnyFloat16Array AnyFloat16DType
half
float32 AnyFloat32Array AnyFloat32DType
single
float64 AnyFloat64Array AnyFloat64DType
double
longdouble AnyLongDoubleArray AnyLongDoubleDType
复数
numpy._ optype.numpy._
标量类型 基本类型 数组类型 dtype 类型
complex64 复浮点数 AnyComplex64Array AnyComplex64DType
csingle
complex128 AnyComplex128Array AnyComplex128DType
cdouble
clongdouble AnyCLongDoubleArray AnyCLongDoubleDType
"灵活的"

具有“灵活的”长度的标量类型,其值长度取决于特定的 np.dtype 实例化。

numpy._ optype.numpy._
标量类型 基本类型 数组类型 dtype 类型
str_ 字符 AnyStrArray AnyStrDType
bytes_ AnyBytesArray AnyBytesDType
void flexible AnyVoidArray AnyVoidDType
其他类型
numpy._ optype.numpy._
标量类型 基本类型 数组类型 dtype 类型
bool_ 通用类型 AnyBoolArray AnyBoolDType
datetime64 AnyDateTime64Array AnyDateTime64DType
timedelta64 AnyTimeDelta64Array AnyTimeDelta64DType
object_ AnyObjectArray AnyObjectDType
缺失 AnyStringArray AnyStringDType

底层接口

optype.numpy 中有多个 Can*(单方法)和 Has*(单属性)协议,与 NumPy Python API 的 __array_*__ 魔术方法相关。这些类型协议,就像 optype.Can*optype.Has* 一样,是运行时可检查的且可扩展的(即不是 @final)。

[!TIP] 这些协议的所有类型参数都可以省略,这相当于传递其上界类型。

协议类型签名 实现 NumPy 文档
class CanArray[
    ND: tuple[int, ...] = ...,
    ST: np.generic = ...,
]
def __array__[RT = ST](
    _,
    dtype: DType[RT] | None = ...,
) -> Array[ND, RT]

用户指南:与 NumPy 的互操作性

class CanArrayUFunc[
    U: UFunc = ...,
    R: object = ...,
]
def __array_ufunc__(
    _,
    ufunc: U,
    method: LiteralString,
    *args: object,
    **kwargs: object,
) -> R

NEP 13

class CanArrayFunction[
    F: CanCall[..., object] = ...,
    R = object,
]
def __array_function__(
    _,
    func: F,
    types: CanIterSelf[type[CanArrayFunction]],
    args: tuple[object, ...],
    kwargs: Mapping[str, object],
) -> R

NEP 18

class CanArrayFinalize[
    T: object = ...,
]
def __array_finalize__(_, obj: T)

用户指南:子类化 ndarray

class CanArrayWrap
def __array_wrap__[ND, ST](
    _,
    array: Array[ND, ST],
    context: (...) | None = ...,
    return_scalar: bool = ...,
) -> Self | Array[ND, ST]

API:标准数组子类

class HasArrayInterface[
    V: Mapping[str, object] = ...,
]
__array_interface__: V

API:数组接口协议

class HasArrayPriority
__array_priority__: float

API:标准数组子类

class HasDType[
    DT: DType = ...,
]
dtype: DT

API:指定和构造数据类型

项目详情


下载文件

下载适用于您平台的文件。如果您不确定要选择哪一个,请了解更多关于 安装包 的信息。

源分布

optype-0.6.1.tar.gz (74.2 kB 查看哈希值)

上传时间

构建分布

optype-0.6.1-py3-none-any.whl (62.5 kB 查看哈希值)

上传时间 Python 3

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面