输入注释类变量:init还是body?

时间:2019-12-25 13:09:58

标签: python type-hinting mypy

让我们考虑以下两种语法变体:

class Foo:

    x: int

    def __init__(self, an_int: int):
        self.x = an_int

class Foo:

    def __init__(self, an_int: int):
        self.x = an_int

显然,以下代码在两种情况下都会引发mypy错误(这是预期的):

obj = Foo(3)
obj.x.title()  # this is a str operation

但是我真的很想执行合同:我想明确一点,x是每个Foo对象的实例变量。那么应该首选哪种语法,为什么呢?

2 个答案:

答案 0 :(得分:2)

这最终是个人喜好的问题。要在其他答案中使用该示例,请同时执行以下操作:

class Foo:
    x: Union[int, str]

    def __init__(self, an_int: int) -> None:
        self.x = an_int

...然后做:

class Foo:
    def __init__(self, an_int: int) -> None:
        self.x: Union[int, str] = an_int

...类型检查器将以完全相同的方式处理

使用前一种方法的主要优点是,在构造函数复杂到难以跟踪正在执行哪种类型推断的情况下,它使属性的类型更加明显。

此样式还与您声明和使用dataclasses之类的方式保持一致:

from dataclasses import dataclass

@dataclass
class Foo:
    x: int
    y: Union[int, str]
    z: str

# You get an `__init__` for free. Mypy will check to make sure the types match.
# So this type checks:
a = Foo(1, "b", "c")

# ...but this doesn't:
b = Foo("bad", 3.14, 0)

这实际上不是一个利弊,只是更多观察到标准库在某些特定情况下采用了以前的样式。

主要缺点是此样式有点冗长:您被迫两次重复变量名(如果包含__init__参数则被重复三次),并且经常被强制重复两次类型提示(一次出现在变量注释中,一次出现在__init__签名中。

它还会在您的代码中带来一个可能的正确性问题:mypy永远不会实际检查以确保您已为属性分配了任何内容!例如,尽管以下代码在运行时崩溃,但仍将很高兴地键入check:

class Foo:
    x: int

    def __init__(self, x: int) -> None:
        # Whoops, I forgot to do 'self.x = x'
        pass

f = Foo(1)

# Type checks, but crashes at runtime!
print(f.x)

后一种样式可避免这些问题:如果忘记分配属性,mypy将在以后尝试使用该属性时抱怨它不存在。

后一种样式的另一个主要优点是,您也可以不必花费很多时间不添加显式类型提示,尤其是在直接将参数分配给字段的情况下。在这种情况下,类型检查器会推断出完全相同的类型。


因此,考虑到这些因素,我个人比较喜欢:

  1. 如果我只想要一个具有自动生成的__init__的简单,类似记录的对象,请使用数据类(并通过代理使用以前的样式)。
  2. 如果我感觉数据类过大或需要编写自定义__init__,请使用后一种样式,以减少冗长和出现“忘记分配属性”错误的几率。
  3. 如果我有一个足够大而复杂的__init__,有点难以阅读,请切换回以前的样式。 (或更妙的是,只需重构代码即可使__init__变得简单!)

您最终可能会权衡这些因素,并得出一系列不同的权衡。


最后一条切线-完成时:

class Foo:
    x: int

...您实际上并未在注释 class变量。此时,x没有值,因此实际上不作为变量存在。

您要创建的唯一内容是注释,它只是纯元数据,与变量本身不同。

但是,如果您这样做:

class Foo:
    x: int = 3

...然后您 创建一个类变量和一个注释。有点令人困惑,尽管您可能正在创建 class 变量/属性(与 instance 变量/属性相对),但是mypy和其他类型检查器将继续假设类型注释为旨在专门注释 instance 属性。

这种不一致在实践中通常并不重要,尤其是如果您遵循避免对任何事物使用可变默认值的一般最佳实践。但是,如果您想做一些花哨的事情,这可能会引起一些意外。

如果您希望mypy /其他类型检查器将您的注释理解为类变量注释,则需要使用ClassVar类型:

# Import this from 'typing_extensions' if you're using Python 3.7 or earlier
from typing import ClassVar

class Foo:
    x: ClassVar[int] = 3

答案 1 :(得分:1)

如果您想对实例变量使用AnyUnionOptional,则应为它们加上注释:

from typing import Union

class Foo:

    x: Union[int, str]

    def __init__(self, an_int: int):
        self.x = an_int

    def setx(self, a_str: str):
        self.x = a_str

否则,您可以使用任何您认为更容易阅读的内容。 mypy将从__init__推断类型。