mypy、compose 方法和重复代码的问题

Issues with mypy, compose method and duplicated code

我已经尝试寻找这个问题的答案,因为我不可能是第一个偶然发现这个问题的人,但我的 google-fu 让我非常失望。

是否可以让 mypy 理解某些 functions/methods 只能从另一个方法中调用?

让我们以下面的代码为例(E:为清楚起见 - 这是一个简化的、可运行的现实生活问题版本,涉及一个有意为空字段的 Django 模型实例):

from typing import Optional


class Foo:
    def __init__(self, value: Optional[int] = None) -> None:
        self.value = value


def compose_method(foo: Foo) -> None:
    if not foo.value:
        raise RuntimeError("Oh no!")

    __method_1(foo)
    __method_2(foo)


def __method_1(foo: Foo) -> None:
    foo.value + 1


def __method_2(foo: Foo) -> None:
    foo.value + 2

运行 mypy 在这个文件上不出所料地导致:

test_mypy.py:18: error: Unsupported operand types for + ("None" and "int")
test_mypy.py:18: note: Left operand is of type "Optional[int]"
test_mypy.py:22: error: Unsupported operand types for + ("None" and "int")
test_mypy.py:22: note: Left operand is of type "Optional[int]"

这显然是正确的,但它没有考虑到这些方法仅打算从 compose_method 中调用这一事实 - 这已经检查了 value != None。我不想复制代码并在每个方法中放置 if not foo.value: 条件,其中 foo.value 只是为了满足工具的要求。同时,# type: ignore 也不太适合我。

是否有一些最好的 practice/way 来处理社区认为好的和正确的?或者我只是注定要复制代码(当然,检查可以放在一个单独的方法中并在 compose_method 包含的每个方法中调用)或使用 # type: ignore 注释?

你可以这样写 __method_1__method_2 只从 compose_method 中调用,但是,据 mypy 所知,有人会导入你的文件并直接调用这些方法。请记住 Python 并没有真正的私人项目的概念。

你可以做的是通过使用 typing.cast.[= 告诉 mypy 本质上,“我保证 value 不是 None”来消除错误。 21=]

def __method_1(foo: Foo) -> None:
    foo.value = typing.cast(int, foo.value) + 1

此处轻微 性能下降。由于这不使用 +=,Python 编译器不会发出 INPLACE_ADD 字节码。我原本想改为 typing.cast(int, foo.value) += 1。但是,通过函数调用,它不再是 L 值,因此成为无效语法。

正因为如此,最好的办法可能就是 # type: ignore

如果使用assert,优点是当python以-O选项启动时assert消失。

from typing import Optional


class Foo:
    def __init__(self, value: Optional[int] = None) -> None:
        self.value = value


def compose_method(foo: Foo) -> None:
    if not foo.value:
        raise RuntimeError("Oh no!")

    __method_1(foo)
    __method_2(foo)


def __method_1(foo: Foo) -> None:
    assert isinstance(foo.value, int)  
    foo.value + 1


def __method_2(foo: Foo) -> None:
    assert isinstance(foo.value, int)  
    foo.value + 2