使用 Python Typing 实现泛型类型依赖

使用 Python Typing 实现泛型类型依赖

本文介绍了如何使用 python 的 typing 模块来实现泛型类型之间的依赖关系。通过使用 Protocol 和 TypeVar,我们可以更精确地定义类的类型约束,从而提高代码的可读性和健壮性。本文提供了一个具体的例子,展示了如何将 to 参数的类型与 data 参数的类型绑定在一起,并提供了详细的代码示例和使用方法。

在 Python 中,类型提示可以显著提高代码的可读性和可维护性。然而,当涉及到泛型类型时,特别是当这些类型之间存在依赖关系时,类型提示可能会变得复杂。本文将介绍如何使用 typing 模块中的 Protocol 和 TypeVar 来解决这类问题。

使用 Protocol 定义索引类型

当我们需要定义一个类型,它必须支持某种索引操作(例如,__getitem__ 和 __setitem__),但我们不关心它的具体类型是 MutableMapping 还是 MutableSequence 时,可以使用 Protocol。

以下是一个 Indexable 协议的定义,它要求类型必须支持使用键 K 进行索引,并且可以设置对应的值:

立即学习Python免费学习笔记(深入)”;

import typing  K = typing.TypeVar('K', contravariant=True) class Indexable(typing.Protocol[K]):     def __getitem__(self, key: K):         pass      def __setitem__(self, key: K, value: typing.Any):         pass

这里,K 是一个 TypeVar,它表示键的类型。contravariant=True 表明 K 是逆变的,这意味着 Indexable[int] 可以赋值给 Indexable[typing.Hashable]。

使用 TypeVar 约束泛型类型

TypeVar 可以用来定义泛型类型变量,并可以对这些变量进行约束。例如,我们可以定义一个 TypeVar,它必须是 typing.Hashable 的子类型:

H = typing.TypeVar('H', bound=typing.Hashable)

这表示 H 可以是任何可哈希的类型,例如 int、str 等。

将 to 和 data 类型绑定

现在,我们可以使用 Protocol 和 TypeVar 来定义 ApplyTo 类,并将 to 参数的类型与 data 参数的类型绑定在一起:

import typing  DispatchType = typing.Literal['separate', 'joint']  # `P` must be declared with `contravariant=True`, otherwise it errors with # 'Invariant type variable "P" used in protocol where contravariant one is expected' K = typing.TypeVar('K', contravariant=True) class Indexable(typing.Protocol[K]):     def __getitem__(self, key: K):         pass      def __setitem__(self, key: K, value: typing.Any):         pass  # Accepts only hashable types (including `int`s) H = typing.TypeVar('H', bound=typing.Hashable) class ApplyTo(typing.Generic[H]):     _to: typing.Sequence[H]     _dispatch: DispatchType     _transform: typing.Callable[..., typing.Any]  # TODO Initialize `_transform`      def __init__(self, to: typing.Sequence[H] | H, dispatch: DispatchType = 'separate') -> None:         self._dispatch = dispatch         self._to = to if isinstance(to, typing.Sequence) else [to]      def __call__(self, data: Indexable[H]) -> typing.Any:         if self._dispatch == 'separate':             for key in self._to:                 data[key] = self._transform(data[key])             return data          if self._dispatch == 'joint':             args = [data[key] for key in self._to]             return self._transform(*args)          assert False

在这个例子中,ApplyTo 类接受一个泛型类型 H,它必须是 typing.Hashable 的子类型。_to 属性的类型是 typing.Sequence[H],表示它是一个包含 H 类型元素的序列。__call__ 方法接受一个 Indexable[H] 类型的参数 data,这意味着 data 必须支持使用 H 类型的键进行索引。

使用示例

以下是一些使用 ApplyTo 类的示例:

def main() -> None:     r0 = ApplyTo(to=0)([1, 2, 3])     # typechecks     r0 = ApplyTo(to=0)({1: 'a', 2: 'b', 3: 'c'})     # typechecks      r1 = ApplyTo(to='a')(['b', 'c', 'd'])     # does not typecheck: Argument 1 to "__call__" of "Applier" has incompatible type "list[str]"; expected "Indexable[str]"     r1 = ApplyTo(to='a')({'b': 1, 'c': 2, 'd': 3})      # typechecks

可以看到,当 to 是 int 类型时,data 可以是 list 或 dict,但当 to 是 str 类型时,data 必须是 dict。这是因为 list 只支持使用 int 类型的索引,而 dict 支持使用 Hashable 类型的索引。

总结

通过使用 Protocol 和 TypeVar,我们可以更精确地定义泛型类型之间的依赖关系,从而提高代码的可读性和健壮性。在设计泛型类时,应该仔细考虑类型之间的约束关系,并使用 typing 模块提供的工具来表达这些约束。这有助于在编译时发现类型错误,并减少运行时错误的发生。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享