键入注释 class 变量:在 init 还是 body 中?
Type annotating class variable: in init or body?
让我们考虑以下两种语法变体:
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
对象的实例变量。那么应该首选哪种语法,为什么?
如果您想将 Any
, Union
, or Optional
用于实例变量,您应该对它们进行注释:
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__
.
推断类型
这最终是个人喜好问题。要使用其他答案中的示例,请执行以下操作:
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 永远不会实际检查以确保您已为属性分配了任何内容!例如,下面的代码尽管在运行时崩溃了,但仍会愉快地进行类型检查:
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 会抱怨它不存在。
后一种风格的另一个主要优点是,您也可以在很多时候不添加显式类型提示,尤其是当您只是直接将参数分配给字段时。在这些情况下,类型检查器将推断出完全相同的类型。
所以考虑到这些因素,我个人的偏好是:
- 如果我只想要一个简单的、类似记录的对象并自动生成
__init__
. ,请使用数据classes(并通过代理,前一种方式)
- 如果我觉得 dataclasses 太过分或者需要编写自定义
__init__
,请使用后一种样式,以减少冗长和 运行 进入 "forgot-to-assign-an-attribute" 错误。
- 如果我有足够大和复杂的
__init__
有点难以阅读,请切换回以前的样式。 (或者更好的是,只需重构我的代码,这样我就可以保持 __init__
简单!)
当然,您最终可能会对这些因素进行不同的权衡,并得出一组不同的权衡。
最后一条切线 -- 当您这样做时:
class Foo:
x: int
...您实际上并不是在注释 class 变量。此时,x 没有值,因此实际上不作为变量存在。
您唯一创建的是 注释,它只是纯元数据,与变量本身不同。
但如果你这样做:
class Foo:
x: int = 3
...那么您 是 创建一个 class 变量和一个注解。有点令人困惑,虽然您可能正在创建 class variable/attribute(与 实例 variable/attribute ), mypy 和其他类型检查器将继续假定类型注释旨在专门注释 instance 属性。
这种不一致在实践中通常无关紧要,特别是如果您遵循避免任何事物的可变默认值的一般最佳实践。但是如果你想做一些花哨的事情,这可能会引起一些意外。
如果您希望 mypy/other 类型检查器了解您的注解是 class 变量注解,您需要使用 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
让我们考虑以下两种语法变体:
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
对象的实例变量。那么应该首选哪种语法,为什么?
如果您想将 Any
, Union
, or Optional
用于实例变量,您应该对它们进行注释:
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__
.
这最终是个人喜好问题。要使用其他答案中的示例,请执行以下操作:
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 永远不会实际检查以确保您已为属性分配了任何内容!例如,下面的代码尽管在运行时崩溃了,但仍会愉快地进行类型检查:
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 会抱怨它不存在。
后一种风格的另一个主要优点是,您也可以在很多时候不添加显式类型提示,尤其是当您只是直接将参数分配给字段时。在这些情况下,类型检查器将推断出完全相同的类型。
所以考虑到这些因素,我个人的偏好是:
- 如果我只想要一个简单的、类似记录的对象并自动生成
__init__
. ,请使用数据classes(并通过代理,前一种方式)
- 如果我觉得 dataclasses 太过分或者需要编写自定义
__init__
,请使用后一种样式,以减少冗长和 运行 进入 "forgot-to-assign-an-attribute" 错误。 - 如果我有足够大和复杂的
__init__
有点难以阅读,请切换回以前的样式。 (或者更好的是,只需重构我的代码,这样我就可以保持__init__
简单!)
当然,您最终可能会对这些因素进行不同的权衡,并得出一组不同的权衡。
最后一条切线 -- 当您这样做时:
class Foo:
x: int
...您实际上并不是在注释 class 变量。此时,x 没有值,因此实际上不作为变量存在。
您唯一创建的是 注释,它只是纯元数据,与变量本身不同。
但如果你这样做:
class Foo:
x: int = 3
...那么您 是 创建一个 class 变量和一个注解。有点令人困惑,虽然您可能正在创建 class variable/attribute(与 实例 variable/attribute ), mypy 和其他类型检查器将继续假定类型注释旨在专门注释 instance 属性。
这种不一致在实践中通常无关紧要,特别是如果您遵循避免任何事物的可变默认值的一般最佳实践。但是如果你想做一些花哨的事情,这可能会引起一些意外。
如果您希望 mypy/other 类型检查器了解您的注解是 class 变量注解,您需要使用 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