【问题标题】:Is PEP 585 unusable at runtime under Python 3.7 and 3.8?在 Python 3.7 和 3.8 下,PEP 585 在运行时无法使用吗?
【发布时间】:2020-07-13 07:58:57
【问题描述】:

PEP 585 -- Type Hinting Generics In Standard Collections 声明在 Python 3.7 和 3.8 下都具有标准 from __future__ import annotations 序言的可用性。值得注意的是:

对于仅限于类型注释的用例,带有 annotationsfuture-import(自 Python 3.7 起可用)的 Python 文件可以参数化标准集合,包括内置函数。

从 Python 3.7 开始,当使用from __future__ import annotations 时,函数和变量注解可以直接参数化标准集合。示例:

from __future__ import annotations

def find(haystack: dict[str, list[int]]) -> int:
    ...

虽然上面的玩具示例确实在技术上进行了解析,但这就是它所做的全部。在 Python 3.7 或 3.8 下尝试在运行时实际使用参数化的内置集合总是会引发可怕的 TypeError: 'type' object is not subscriptable 异常:

>>> def find(haystack: dict[str, list[int]]) -> int: pass
>>> print(find.__annotations__)
{'haystack': 'dict[str, list[int]]', 'return': 'int'}
>>> eval(find.__annotations__['haystack'])
TypeError: 'type' object is not subscriptable

注意eval() 语句是解析PEP 563-style postponed annotations at runtime 的标准习惯用法。 甚至不要让我开始使用 PEP 563。

你会相信谁:我还是你撒谎的 PEP?

这让我心中虔诚的 Pythonista 望而却步。 PEP 585 repeatedly claims that it preserves runtime usability:

在运行时保留泛型类型可以对可用于 API 生成或运行时类型检查的类型进行自省。这种用法已经在野外存在。

就像今天的typing 模块一样,上一节中列出的参数化泛型类型都在运行时保留了它们的类型参数:

>>> list[str]
list[str]
>>> tuple[int, ...]
tuple[int, ...]
>>> ChainMap[str, list[str]]
collections.ChainMap[str, list[str]]

当然,以上方法在 Python 3.7 或 3.8 下都不起作用——无论from __future__ import annotations 是否启用:

>>> list[str]
TypeError: 'type' object is not subscriptable
>>> tuple[int, ...]
TypeError: 'type' object is not subscriptable
>>> ChainMap[str, list[str]]
TypeError: 'type' object is not subscriptable

所以 PEP 585 公然打破了在运行时自省泛型类型的狂野和所有现有尝试——尤其是来自运行时类型检查器。整个“泛型参数在运行时可用”部分是一个字谜。

我是否遗漏了一些非常明显的东西,或者参数化的内置集合是它们表面上看起来的毒丸?由于在 Python 3.7 和 3.8 下在运行时评估这些集合会无条件地引发异常,因此它们在运行时不可用 - 不仅使它们毫无用处,而且对类型自省和运行时类型检查的广泛用例尤其是直接有害.

在岩石和硬 PEP 之间

任何带有参数化内置集合的代码库类型提示都将与 Python 3.7 和 3.8 下的运行时类型检查器根本不兼容。代码库更喜欢运行时而不是静态类型检查,同时保持与 Python

除了那也是不可行的。为什么?因为PEP 585 deprecates the entire hierarchy of typing pseudo-containers

不推荐从 typing 导入这些 [例如,typing.Tupletyping.Listtyping.Dict]。 由于 PEP 563 和旨在最大限度地减少 typing 的运行时影响,此弃用不会生成DeprecationWarnings。相反,当被检查程序的目标版本被通知为 Python 3.9 或更高版本时,类型检查器可能会警告这种不推荐使用的用法。建议允许在整个项目范围内消除这些警告。

在 Python 3.9.0 发布 5 年后发布的第一个 Python 版本中,已弃用的功能将从 typing 模块中删除。

typing.Tuple[int] 为例。到 2025 年(或此后不久),typing.Tupletyping.Tuple[int] 将消失。但是tuple 在 Python 3.7 和 3.8 下不能安全地参数化,因为这样做会使您的项目与内省类型的任何东西不兼容。所以tuple[int] 也不是一个可行的选择。

因此没有向前和向后兼容的选项。相反,要么:

  • 禁止类型自省(并因此禁止运行时类型检查)完全通过只首选内置容器(例如,tuple[int])而不是typing 伪容器(例如,typing.Tuple[int]嗯>...
  • 支持类型自省(以及运行时类型检查),方法是:
    • 在 2025 年之前,优先使用 typing 伪容器而不是内置容器。届时,有问题的项目该项目的所有下游项目都需要进行如下重构:
      • 放弃对 Python 3.7 和 3.8 的支持。
      • 用内置容器替换所有typing 伪容器。
    • 通过首选内置容器而不是typing 伪容器,立即放弃对 Python 3.7 和 3.8 的支持。这有一个令人讨厌的缺点,即需要当前不稳定的 Python 解释器,但是……从技术上讲,这是一种选择。不知何故。

在 2020 年,没有什么好的选择——只有一系列越来越可怕的恶毒中的小恶魔。人们希望 PEP 作者能够在运行时实际测试他们的实现。然而,我们在这里,在理论制造的反 API 的热气腾腾的粪坑中漂流,没有桨。 欢迎使用 Python。

但这还不是全部

技术上还有第三种方式。它甚至更多令人反感——但它应该在技术上有效。我总是说,一个糟糕的理论设计值得另一个!

由于 PEP 563 驱动的延迟注释只是字符串,类型自省可以巧妙地对每个被自省的类型运行基于正则表达式的替换。对于作为延迟注释的每种类型,将该注释字符串中引用参数化内置容器(例如list[str])的每个子字符串全局替换为引用参数化typing伪容器(例如List[str])的相应子字符串。

结果?与 Python 3.7 和 3.8 兼容的延迟注释字符串可安全评估到 2025 年,届时内部替换(以及对 Python 3.7 和 3.8 的支持)可能会被悄悄放弃。

对于星星来说,这完全是一个疯狂的可笑速度组合,但是......可能会奏效。当然,核心问题是人们不应该仅仅为了遵守核心官方 PEP 而需要疯狂的骇客。但在那个技术问题之下还有一个更深层次的文化问题。没有人——无论是 PEP 585 的作者还是任何审查 PEP 585 的评论员——在弃用现有的经过良好测试但实际工作的功能之前,实际测试了他们提出的新假设功能。

核心官方 PEP 应该工作开箱即用。越来越多,他们没有。这应该引起每个人的关注。

【问题讨论】:

  • 这里可能有一个合理的问题,但现在它看起来更像是对 PEP 质量的咆哮。请大幅削减。
  • Python 注释系统的整个设计几乎不关心运行时支持。他们一直到 Python 3.8 才引入typing.get_args。如果运行时支持在优先级列表中的任何位置,那么自第一个 typing 版本以来就会存在这种情况。

标签: python python-3.x python-typing pep585


【解决方案1】:

我不知道你为什么要在 StackOverflow 上发布这个?如果您有关于 PEP 的反馈,我认为您最好将其发布在 python-dev 或 typing-sig 邮件列表中。

例如,您也许可以尝试争论:

  1. 我们应该在 2025 年之后移除伪容器。
  2. typing_extensions 模块应该更新为list 提供垫片 和 dict 复制将在 Python 3.9 中添加的运行时功能。
  3. 最好将typing 中的伪容器替换为内置函数的别名,而不是删除它们。 (我怀疑这很可能最终会发生:例如,参见 this discussion in typing-sig

...等等。我相信你能想到更多的想法,这些只是我现在想出来的。

无论如何,我认为处理从typing 中删除的伪容器应该很容易,同时保持向后兼容性。例如,您可以尝试:

  1. 编写一个工具来自动更新您的代码(假设该工具尚未由某人创建)。

  2. 修补猴子 typing 以补充缺失的内容。

  3. 提前切换到使用您自己的typing shim,它执行以下操作:

    from typing import *
    if sys.version_info >= (3, blah):
        List = list
        # etc
    

当然,这些方法也不是完全干净,但它们确实可以让您实现确保完全向后兼容性的目标。

“禁止类型自省并提前切换”的方法在实践中也可能相当合理:我怀疑相当大比例的代码库将类型提示专门用于静态类型分析。

但更广泛地说,如果您希望对类型提示进行更无缝的运行时自省,我建议您订阅 typing-sig 或 python-dev,并在提出与类型相关的 PEP 时提供您的反馈。

到目前为止,我认为没有人足够关心类型提示的运行时自省,除了确保继续为它们提供基线支持之外,还没有做任何事情。如果您对这种现状不满意,您应该尝试站出来支持您认为需要做出的任何改变。

毕竟,Python 是一个志愿者驱动的项目。因此,如果您想改变 Python 的某些方面,最好的方式是自愿付出您的时间和精力,而不是等待其他人代表您这样做。

【讨论】:

  • 嗯,是一个令人惊讶的对抗性回复。我不是“等待其他人”代表我“改变有关 Python 的一些东西”。我试图理解为什么一个被宣传为有效的 PEP 实际上不能像宣传的那样工作(或根本没有)——以及我在这一点上是否可能弄错了。显然,我不是。我也不同意对类型提示的运行时自省“仍然有基线支持”。不存在这样的支持。 typing 模块及其关联者公开反对运行时内省(例如,故意禁止 isinstanceissubclass 检查)。
  • 感谢@Michael0x2a 的解决方案。但是请注意,您的示例引发了一个 mypy 错误Cannot assign multiple types to name "List" without an explicit "Type[...]" annotation,并且星型导入并不总是最好的。
  • @HarenS 从技术上讲,它应该用if..else 拆分到sys.version_info 检查,而不是多次分配给同一个变量名。
猜你喜欢
  • 2020-08-31
  • 2023-03-31
  • 2021-02-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多