我有一个属性,每个实例仅计算一次,并且使用None作为保护值。这是带有属性的非常常见的模式。

注释的最佳方法是什么?

编辑/说明:

这个问题是关于如何使用mypy来验证我属性(property)的代码。与如何重构代码无关。该属性的类型就是我想要的方式,它是一个非可选的int。例如,假设下游代码将执行print(book.the_answer+1)

字符串的错误分配和无值是完全有意破坏属性期望所定义的契约(Contract),我希望mypy对其进行标记。

尝试#1

class Book:

    def __init__(self, title):
        self.title = title

    _the_answer = None #line 6
    # _the_answer : int = None #line 7

    def __str__(self):
        return "%s => %s" % (self.title, self.the_answer)

    @property
    def the_answer(self)->int:
        """always should be an int.  Not an Optional int"""
        if self._the_answer is None:
            if "Guide" in self.title:
                #this is OK
                self._the_answer = 42
            elif "Woolf" in self.title:
                #this is wrong, mypy should flag it.
                self._the_answer = "?, I haven't read it"  # line 21
            else:
                #mypy should also flag this
                self._the_answer = None #line 24

        return self._the_answer #line 26

print(Book("Hitchhiker's Guide"))
print(Book("Who's afraid of Virginia Woolf?"))
print(Book("War and Peace"))

输出:
Hitchhiker's Guide => 42
Who's afraid of Virginia Woolf? => ?, I haven't read it
War and Peace => None

Mypy的输出:
test_prop2.py:21: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[int]")
test_prop2.py:26: error: Incompatible return value type (got "Optional[int]", expected "int")

第26行的确没有错,这完全取决于IF中分配的内容。 21和24都不正确,但是mypy仅捕获21。

注意:如果我将属性更改为return cast(int, self._the_answer) #line 26,则至少将其保留下来。

如果将类型添加到保护值_the_answer:

尝试#2,也输入保护值:
class Book:

    def __init__(self, title):
        self.title = title

    #_the_answer = None
    _the_answer : int = None #line 7

    def __str__(self):
        return "%s => %s" % (self.title, self.the_answer)

    @property
    def the_answer(self)->int:
        """always should be an int.  Not an Optional int"""
        if self._the_answer is None:
            if "Guide" in self.title:
                #this is OK
                self._the_answer = 42
            elif "Woolf" in self.title:
                #this is wrong.  mypy flags it.
                self._the_answer = "?, I haven't read it"  # line 21
            else:
                #mypy should also flag this
                self._the_answer = None #line 24

        return self._the_answer #line 26

print(Book("Hitchhiker's Guide"))
print(Book("Who's afraid of Virginia Woolf?"))
print(Book("War and Peace"))

运行输出相同,但是mypy具有不同的错误:
test_prop2.py:7: error: Incompatible types in assignment (expression has type "None", variable has type "int")

并且它没有键入检查行21、24和26(实际上它只键入了一次,但是后来我更改了代码,此后没有)。

而且,如果我将第7行更改为_the_answer : int = cast(int, None),则mypy完全保持沉默,并没有任何警告。

版本:
mypy   0.720
Python 3.6.8

最佳答案

尽管mypy可能并未标记要标记的确切行,但它合法地报告您的the_answer实现不正确,并且不能保证返回int。

您的两个错误:

test_prop2.py:21: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[int]")
test_prop2.py:26: error: Incompatible return value type (got "Optional[int]", expected "int")

...实际上是正确的。 Mypy推断您的_the_answer字段具有Optional[int]的类型,因为第一次尝试为该字段分配non-None值时,您分配了一个int。 (您的if语句的第一个分支)。

第二个错误也是正确的:由于您在第24行将None分配给self._the_answer,因此在某些情况下,返回值显然将是None。因此,mypy报告您在实际尝试返回字段时遇到错误。

mypy实际上在第24行报告错误是不正确的。由于_the_answer被推断为Optional[int]类型,因此向该字段分配None值实际上没有任何内在错误。因此,mypy推迟了该错误,直到它确定知道出了点问题为止。

请注意,如果您实际修复了代码,mypy将停止报告任何错误。例如,以下程序对类型进行了干净检查:

class Book:
    def __init__(self, title):
        self.title = title

    _the_answer = None

    def __str__(self):
        return "%s => %s" % (self.title, self.the_answer)

    @property
    def the_answer(self) -> int:
        if self._the_answer is None:
            if "Guide" in self.title:
                self._the_answer = 42
            else:
                self._the_answer = 100

        return self._the_answer

在这里,mypy能够推论到我们返回返回值时,在所有情况下self._the_answer始终是int,因此意识到在该函数中_the_answer的类型可以暂时缩小为int

当然,Mypy的类型推断算法并不完美。如果您的实际代码更复杂,则mypy似乎无法推断正确的事情,您可以通过添加一些assert isinstance(self._the_answer, int)来强制mypy缩小类型,从而帮助实现这一目标。

您可以通过添加一些reveal_type(self._the_answer)来随时检查mypy认为您的字段的类型是什么-当mypy遇到该伪函数时,除了报告任何类型错误之外,它还将报告它认为该表达式是什么类型。

有点不相关,我还建议您显式注释_the_answer字段-例如在第七行执行_the_answer: Optional[int] = None

现在,mypy仅通过检查第一个non-None分配就可以推断_the_answerOptional[int]。但是,如果您要重新排列代码,以使您不小心将(错误的)字符串分配放在首位,则mypy会推断类型为Optional[str]。这显然有些脆弱。

关于python - 属性注解,用于检查对初始设置为“无”的保护值的分配,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/57387831/

10-12 19:28