mypy 不喜欢Cyhton类型的别名

4

我想使用Cython加速一个PEP 484类型的Python脚本。我希望保持一些语义和可读性。

之前,我有一个

Flags = int

def difference(f1: Flags, f2: Flags):
    return bin(f1 ^ f2).count("1")

现在这个函数被频繁调用,很自然地考虑进行轻微的重构并使用Cython编译,但是我不想失去f1f2是标志位集合的信息。因此,我显然尝试了

import cython

Flags = cython.int

def difference(f1: Flags, f2: Flags):
    return bin(f1 ^ f2).count("1")

现在,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?)

如果没有这个类型别名

import cython

def difference(f1: cython.int, f2: cython.int):
    return bin(f1 ^ f2).count("1")

该模块检查良好(除了缺少cython库存根文件之外)。

这是怎么回事?类型别名的目的不就是在后续行为上没有任何差异吗?

1个回答

8
你在这里遇到的问题是由于cython没有关联类型提示,所以不幸的是无法确定cython.int表达式的确切含义--因此也就无法确定Flags = cython.int的意思。
特别地,cython.int可能被视为一个值,而不是一种类型。在这种情况下,Flags = cython.int将只是普通的变量赋值,而不是类型别名。
虽然理论上来说mypy可以尝试分析您程序的其余部分以解决这种歧义,但这样做可能会有点昂贵。因此,它会有点任意地决定cython.int必须是一个值(例如常量),这反过来导致你的difference函数不能通过类型检查。
然而,如果您直接在类型签名中使用cython.int类型,则没有这种歧义:在那种情况下,该表达式很可能是某种类型,因此mypy决定以另一种方式解释该表达式。
那么,如何解决这个问题?嗯,有几个方法可以尝试,我会按照递减的顺序列出它们(并且增加了hackyness)。
  1. Submit a pull request to mypy implementing support for PEP 613. This PEP is intended to give users a way of directly resolving this ambiguity by letting them directly indicate whether something is supposed to be a type alias or not.

    This PEP has been accepted; the only reason why mypy doesn't support it is because nobody has gotten around to implementing it yet.

  2. Ask the Cython maintainers if they'd be ok with shipping stub files for cython by turning their package into a PEP 561 compliant package -- a package that comes bundled with type hints.

    It seems Cython is already bundling some type hints in a limited way, and making them available for external use may theoretically be as simple as testing to make sure they're still up-to-date and adding a py.typed file to the Cython package.

    More context about type hints in Cython can be found here and here.

    Mypy is also planning on overhauling how imports are handled so you can optionally use any bundled type hints even if the package isn't PEP 561 compliant during the next few months -- you could also wait for that to happen.

  3. Create your own stubs package for cython. This package can be incomplete and define only int and a few other things you need. For example, you could create a "stubs/cython.pyi" file that looks like this:

    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
    

    Then, point mypy at this stub file in addition to your usual code. Mypy will then understand that it should use this stub file to serve as type hints for the cython module. This means that when you do cython.int, mypy will see that it's the class you defined up above and so will have enough information to know that Flags = cython.int is likely a type alias.

  4. Redefine what Flags is assigned when performing type checking only. You can do this via the typing.TYPE_CHECKING variable:

    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")
    

    One caveat to this approach is that I'm not sure to what degree Cython supports these sorts of PEP 484 tricks and whether it'll recognize that Flags is meant to be a type alias if it's wrapped in an if statement like this.

  5. Instead of making Flags a type alias for cython.int, make it a subclass:

    import cython
    
    class Flags(cython.int): pass
    
    def foo(a: Flags, b: Flags) -> Flags:
        return a ^ b
    

    Now, you're using cython.int back in a context where it's reasonable to assume it's a type, and mypy ends up not reporting an error.

    Granted, this does change the semantics of your program and may also make Cython unhappy -- I'm not really familiar with how Cython works, but I suspect you're not really meant to subclass cython.int.


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接