如何操作内置函数的类型提示

How to manipulate a builtin's type hinting

我使用 ElementTree 到 parse/build 一些稍微复杂但定义明确的 xml 文件,并使用 mypy 进行静态类型化。我到处都是 .find 语句,这导致了这样的事情:

from xml.etree.ElementTree import Element
...
root.find('tag_a').append(Element('tag_b'))

# run mypy..
-> type None from Optional[Element] has no attribute append

这是有道理的,因为 find 根本找不到我给它的标签。但我知道它就在那里,并且不想添加 try..exceptassert 语句之类的东西,本质上只是简单地使 mypy 静音,而不添加功能,同时降低代码的可读性。我也想避免到处评论 # type: ignore


我尝试了猴子补丁 Element.find.__annotations__,我认为这是一个很好的解决方案。但是因为它是内置的,所以我不能那样做,而且子类化 Element 又感觉太多了。

有什么好的方法可以解决吗?

Mypy 不使用 __annotations__,这是一个运行时构造。 Mypy 的分析是完全静态的。

"builtin" 类型(又名标准库中的类型)来自 typeshed。如果您希望为自己的目的修改这些类型,您可以(尽管我强烈反对将其作为您问题的解决方案)。要在 mypy 中使用自定义类型,您可以 mypy --custom-typeshed-dir=/path/to/my/typeshed ... 并且 mypy 将使用您修改后的类型。

一个更符合人体工程学的解决方案是按照 Azat 的建议进行操作,并编写一个将类型缩小移动到实用函数的包装器,这样本地可读性就不会受到影响,并且您可以保持类型安全。

我们可以编写一个实用函数,它在内部处理 None-found 案例并引发 exception/returns 一些给定类型的虚拟值:

from xml.etree.ElementTree import Element


def find(element: Element,
         tag: str) -> Element:
    result = element.find(tag)
    assert result is not None, ('No tag "{tag}" found '
                                'in element "{element}".'
                                .format(tag=tag,
                                        element=element))
    return result

断言的优势(与手动引发异常相比)是它们 can be disabled 但是 如果您正在处理一些用户提供的数据,我建议引发异常

if result is None:
    raise LookupError('No tag "{tag}" found '
                      'in element "{element}".'
                      .format(tag=tag,
                              element=element))

题外话

我使用类型注释,因为它有助于 IDE 并且在阅读时也节省了很多时间 API,但我不是 mypy 用户,因为我不喜欢这个想法在这种情况下检查所有内容:如果函数用户传递垃圾,那是他的错,我们应该让他这样做而不是写一些关于 "you have a union of types and not handling cases with some of them"、EAFP after all.

的东西

我认为在这里,您可以采取三种不同的选择。

  1. 第一个选项是 中建议的方法:创建一个辅助方法,在运行时显式执行 'None' 检查以满足 mypy.这是最安全的选项。
  2. 第二个选项是配置 mypy 并放宽它处理类型 'None' 的值的方式。目前,mypy 会将 'None' 和 'Element' 视为两种不同的类型:如果您的值是 'None',则它不能是 'Element',反之亦然。实际上,您可以通过给 mypy --no-strict-optional 标志来削弱它,这将使 mypy 将类型 'None' 的值视为 all 类型的成员。

    或者换句话说,如果您熟悉 Java 等语言,那么这样做是合法的:

    String myString = null;
    

    --no-strict-optional 标志传递给 mypy 将使其开始接受上述代码。

    这显然意味着您的代码的类型安全性将降低:mypy 不再能够检测潜在的 "null pointer exceptions"。为了帮助缓解这种情况,您可以通过创建 mypy config file.

    简而言之,您将创建一个大致如下所示的配置文件:

    [mypy]
    # Global options can go here. We'll leave this empty since we don't
    # want to change any of the defaults.
    
    [mypy-mycodebase.my.xml.processing.module]
    # We weaken mypy in *just* this module
    strict_optional = False
    
  3. 第三个选项是完全停止对 XML 解析代码使用静态类型:将 root 变量转换为 'Any' 类型或'object' 然后进城。然后,当您从 XML 收集有用数据时,执行任何必要的运行时检查以验证您的数据并创建(类型安全!)对象来存储相关信息。 (当然,您可以在其余代码中继续使用静态类型)。

    这里的观察是,任何运行时输入本质上都是动态的:用户总是可以输入格式错误的 XML,数据的结构可能不正确,等等......唯一真正的检查方法这些类型的问题正在使用运行时检查:静态类型检查不会有太大帮助。因此,如果静态类型检查在特定代码区域提供的价值最小,为什么还要在那里继续使用它?

    当然,这种策略确实有几个缺点。特别是,mypy 将无法检测到对 ElementTree API 的公然滥用,您需要非常勤奋地进行运行时检查,以确保错误数据不会蔓延到代码的类型检查区域等...