mypy 不喜欢别名 Cython 类型

Ana*_*ory 4 typechecking cython type-alias mypy

我正在尝试使用 Cython 加速 PEP 484 类型的 python 脚本。我想保持一些语义和可读性。

之前,我有一个

Flags = int

def difference(f1: Flags, f2: Flags):
    return bin(f1 ^ f2).count("1")
Run Code Online (Sandbox Code Playgroud)

现在这个功能被经常调用,是轻微的重构,并使用用Cython用C编译自然的候选,但我不想失去的信息f1f2一些标志的集合。所以,我显然试过了

import cython

Flags = cython.int

def difference(f1: Flags, f2: Flags):
    return bin(f1 ^ f2).count("1")
Run Code Online (Sandbox Code Playgroud)

现在,mypy失败了,抱怨

flags.py:5: error: Variable "flags.Flags" is not valid as a type
flags.py:5: note: See https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases
flags.py:6: error: Unsupported left operand type for ^ (Flags?)
Run Code Online (Sandbox Code Playgroud)

而没有那个类型别名

import cython

def difference(f1: cython.int, f2: cython.int):
    return bin(f1 ^ f2).count("1")
Run Code Online (Sandbox Code Playgroud)

该模块检查得很好(除了缺少的库存根cython)。

这里发生了什么?类型别名的意义不正是在行为上应该没有区别吗?

Mic*_*x2a 6

您在这里遇到的问题是,由于没有与 cython 相关联的类型提示,不幸的是,表达式的确切cython.int含义Flags = cython.int是不明确的——因此不明确应该是什么意思。

特别是,它可能是cython.int一个value,而不是一个类型。在这种情况下,Flags = cython.int将只是常规变量赋值,而不是类型别名。

虽然理论上 mypy 可以尝试分析程序的其余部分来解决这种歧义,但这样做会有些昂贵。因此,它有点武断地决定cython.int必须是一个值(例如一个常量),这反过来又会导致您的difference函数无法进行类型检查。

但是,如果您cython.int直接在类型签名中使用该类型,则不会出现这种歧义:在这种情况下,该表达式很可能是某种类型,因此 mypy 决定以另一种方式解释该表达式。


那么,你如何解决这个问题?嗯,有几件事你可以尝试,我将按工作量的降序(和 hackyness 的升序)列出。

  1. 向 mypy 提交拉取请求以实现对PEP 613 的支持。此 PEP 旨在通过让用户直接指示某些内容是否应该是类型别名,为用户提供一种直接解决这种歧义的方法。

    此 PEP 已被接受;mypy 不支持它的唯一原因是因为还没有人开始实施它。

  2. 询问 Cython 维护人员是否可以通过将他们的包转换为PEP 561兼容包来传输 cython 的存根文件——一个捆绑了类型提示的包。

    似乎 Cython 已经以有限的方式捆绑了一些类型提示,并且使它们可供外部使用在理论上可能就像测试以确保它们仍然是最新的并将py.typed文件添加到 Cython 包一样简单。

    可以在此处此处找到有关 Cython 中类型提示的更多上下文。

    Mypy 还计划彻底改革导入的处理方式,以便您可以选择使用任何捆绑的类型提示,即使在接下来的几个月中该包不符合 PEP 561 标准——您也可以等待这种情况发生。

  3. 为 cython 创建您自己的存根包。这个包可能不完整,只定义int了一些你需要的其他东西。例如,您可以创建一个如下所示的“stubs/cython.pyi”文件:

    from typing import Any
    
    # Defining these two functions will tell mypy that this stub file
    # is incomplete and to not complain if you try importing things other
    # than 'int'. 
    def __getattr__(name: str) -> Any: ...
    def __setattr__(name: str, value: Any) -> None: ...
    
    class _int:
        # Define relevant method stubs here
    
    Run Code Online (Sandbox Code Playgroud)

    然后,除了通常的代码之外,将 mypy 指向这个存根文件。Mypy 然后会明白它应该使用这个存根文件作为cython模块的类型提示。这意味着当你这样做时cython.int,mypy 将看到它是你在上面定义的类,因此将有足够的信息来知道它Flags = cython.int可能是一个类型别名。

  4. Flags仅在执行类型检查时重新定义分配的内容。您可以通过typing.TYPE_CHECKING变量执行此操作:

    from typing import TYPE_CHECKING
    import cython
    
    # The TYPE_CHECKING variable is always False at runtime, but is treated
    # as being always True for the purposes of type checking
    if TYPE_CHECKING:
        # Hopefully this is a good enough approximation of cython.int?
        Flags = int
    else:
        Flags = cython.int
    
    def difference(f1: Flags, f2: Flags):
        return bin(f1 ^ f2).count("1")
    
    Run Code Online (Sandbox Code Playgroud)

    这种方法的一个警告是,我不确定 Cython 在多大程度上支持这些类型的 PEP 484 技巧,以及Flags如果它被包装在这样的 if 语句中,它是否会识别出这是一个类型别名。

  5. 不要为 制作Flags类型别名cython.int,而是将其设为子类:

    import cython
    
    class Flags(cython.int): pass
    
    def foo(a: Flags, b: Flags) -> Flags:
        return a ^ b
    
    Run Code Online (Sandbox Code Playgroud)

    现在,您cython.int在可以合理假设它是一种类型的上下文中使用back,并且 mypy 最终不会报告错误。

    当然,这确实改变了您程序的语义,并且也可能使 Cython 不高兴——我不太熟悉 Cython 的工作原理,但我怀疑您并不是真的打算将cython.int.