
本文探讨了 mypy 在 `isinstance` 运行时类型检查中,当使用 `@runtime_checkable` 协议的联合类型别名时出现的类型错误。尽管涉及的协议并非参数化泛型,mypy 仍会报告“parameterized generics cannot be used in instance checks”错误。文章通过代码示例对比了报错与正常情况,揭示了此问题是 mypy 的一个已知 bug,并提供了临时的规避方法,以帮助开发者在使用 python 类型提示时避免此类困扰。
在 Python 中,Protocol 提供了一种结构化子类型(Structural Subtyping)的方式,允许我们定义一个接口,而不必通过继承来实现。任何类,只要实现了 Protocol 中定义的所有方法,就被认为是该 Protocol 的一个实现。
@runtime_checkable 装饰器则进一步增强了 Protocol 的功能,使得这些协议可以在运行时被 isinstance() 和 issubclass() 函数检查。这意味着我们可以像检查普通类一样,检查一个对象是否符合某个 Protocol 的结构。例如,typing 模块中的 SupportsInt、SupportsIndex 等都是 @runtime_checkable 的协议,用于检查对象是否支持转换为整数或索引。
在使用 Mypy 进行静态类型检查时,开发者可能会遇到一个特定问题:当尝试在 isinstance() 检查中使用一个由多个 @runtime_checkable 协议组成的联合类型别名时,Mypy 会报告一个错误。
考虑以下场景,我们定义一个自定义的 SupportsTrunc 协议,并将其与 SupportsInt、SupportsIndex 组合成一个联合类型别名 _ConvertibleToInt:
from typing import Protocol, runtime_checkable, SupportsIndex, SupportsInt
@runtime_checkable
class SupportsTrunc(Protocol):
def __trunc__(self) -> int:
...
# 定义一个联合类型别名
_ConvertibleToInt = SupportsInt | SupportsIndex | SupportsTrunc
def process_int_convertible(o: object) -> None:
if isinstance(o, _ConvertibleToInt):
# Mypy 报错:
# error: Parameterized generics cannot be used with class or instance checks
# error: Argument 2 to "isinstance" has incompatible type "<typing special form>"; expected "_ClassInfo"
print(f"Object {o} is convertible to int.")
else:
print(f"Object {o} is not convertible to int.")
# 示例使用
process_int_convertible(10)
process_int_convertible(3.14)
process_int_convertible("hello")尽管 SupportsInt、SupportsIndex 和 SupportsTrunc 都是 @runtime_checkable 协议,并且它们本身并非参数化泛型(如 list[int]),Mypy 仍然会抛出错误,提示“Parameterized generics cannot be used with class or instance checks”。这个错误令人困惑,因为它暗示我们正在使用泛型类型,而实际上并没有。
为了更好地理解这个问题,我们通过对比不同场景下的 Mypy 行为来分析:
直接使用联合类型(不使用别名)
如果我们在 isinstance 检查中直接使用联合类型,而不是通过类型别名,Mypy 不会报错:
from typing import SupportsIndex, SupportsInt
def process_direct_union(o: object) -> None:
if isinstance(o, SupportsInt | SupportsIndex): # Mypy 检查通过
print(f"Object {o} supports int or index conversion.")使用单个协议的类型别名
如果类型别名只指向一个 @runtime_checkable 协议,Mypy 也不会报错:
from typing import SupportsInt
_SingleConvertibleToInt = SupportsInt
def process_single_alias(o: object) -> None:
if isinstance(o, _SingleConvertibleToInt): # Mypy 检查通过
print(f"Object {o} supports int conversion.")使用重复协议的联合类型别名
即使联合类型别名中包含的是同一个协议的重复,Mypy 仍然会报错,这进一步表明问题出在“联合类型别名”这一结构上,而非协议的复杂性:
from typing import SupportsInt
_DuplicateConvertibleToInt = SupportsInt | SupportsInt
def process_duplicate_alias(o: object) -> None:
if isinstance(o, _DuplicateConvertibleToInt): # Mypy 报错
print(f"Object {o} supports int conversion (via duplicate alias).")通过上述示例对比,我们可以明确:Mypy 的错误并非源于 Protocol 本身的参数化,而是其在处理由多个 @runtime_checkable 协议组成的“联合类型别名”作为 isinstance 的第二个参数时,存在一个内部识别问题。
根据 Mypy 社区的反馈,这被确认为 Mypy 的一个已知 bug(例如,在 GitHub issue mypy/#16707 中有相关讨论)。Mypy 在解析 isinstance() 的第二个参数时,对于由 Union 类型别名构成的 @runtime_checkable 集合,未能正确识别其运行时可检查性,从而错误地将其视为“参数化泛型”。
当前的规避方法:
鉴于这是一个 Mypy 的 bug,最直接的规避方法是避免在 isinstance() 检查中使用由多个 @runtime_checkable 协议组成的类型别名。
你可以选择以下两种方式:
直接在 isinstance() 中写出联合类型:
from typing import SupportsIndex, SupportsInt, Protocol, runtime_checkable
@runtime_checkable
class SupportsTrunc(Protocol):
def __trunc__(self) -> int:
...
def process_object(o: object) -> None:
# 直接使用联合类型,避免别名
if isinstance(o, SupportsInt | SupportsIndex | SupportsTrunc):
print(f"Object {o} is convertible to int.")
else:
print(f"Object {o} is not convertible to int.")如果类型别名是必要的,可以考虑在 isinstance 检查时将其展开(虽然这在代码中可能显得冗余,但可以规避 Mypy 错误): 这种方法在Python运行时并不直接支持,因为isinstance的第二个参数期望的是一个类或类的元组。因此,第一种方法是更实际和推荐的。
Mypy 在 isinstance 检查中对 @runtime_checkable 协议的联合类型别名处理不当,是一个已知的静态分析工具限制。虽然这可能会给依赖类型别名来提高代码可读性的开发者带来不便,但通过直接在 isinstance 调用中写出联合类型,可以有效规避此问题。随着 Mypy 的不断发展和完善,我们期待这个 bug 能在未来的版本中得到修复,从而提供更灵活、更符合直觉的类型检查体验。在当前阶段,理解这一限制并采用相应的规避策略,是确保代码能够通过 Mypy 检查的关键。
以上就是Mypy 在 isinstance 中处理联合类型别名的已知问题的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号