我有一个属性,每个实例仅计算一次,并且使用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_answer
是Optional[int]
。但是,如果您要重新排列代码,以使您不小心将(错误的)字符串分配放在首位,则mypy会推断类型为Optional[str]
。这显然有些脆弱。关于python - 属性注解,用于检查对初始设置为“无”的保护值的分配,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/57387831/