考虑一个参数具有可变默认值的数据类。为了能够使用新的默认值实例化一个对象而不是共享可变对象,我们可以这样做:
@dataclass
class ClassWithState:
name: str
items: Optional[List[str]] = None
def __post_init__(self) -> None:
if self.items is None:
self.items = []
这按预期工作。然而,每当我在此类的某些实例中引用
items
时,mypy 都会警告 items
可能为 None。例如:
c = ClassWithState("object name")
c.items.append("item1")
MyPy 会抱怨类似:
“Optional[List[str]]”的“None”项没有属性“append”。
我不想每次提及
items
时都添加不必要的检查,例如
assert c.items is not None
我到处都提到
items
。我怎样才能让 mypy 相信 items
永远不会是 None?
field
与 default_factory
选项集一起使用:
from dataclasses import dataclass, field
from typing import List
@dataclass
class ClassWithState:
name: str
items: List[str] = field(default_factory=list)
>>> ClassWithState("Hello")
ClassWithState(name='Hello', items=[])
问题是我们没有任何方法告诉 mypy
items
在 __post_init__
之前是可选的,但之后不是。
Carcigenicate 的很好的答案 处理所需的默认初始化不依赖于初始化器的其他参数的情况。但是,假设您需要查看
name
才能了解如何默认初始化 items
。
对于这种情况,如果有一个与
default_factory
方法类似的方法,将部分初始化的对象的参数作为参数,那就太好了,但不幸的是 没有这样的模拟。其他看起来相关但没有达到目的的事情:
init=False
字段选项允许在__post_init__
中初始化字段,但删除用户指定显式值的选项。InitVar
泛型类型与我们想要的相反:使值可用于初始值设定项(和 __post_init__
),而不将其包含为数据类对象的字段。但是,作为解决方法,您可以指定一个特殊的对象值来向
__post_init__
方法表示需要替换字段的默认值。 对于大多数类型,很容易创建特定类型的唯一虚拟对象,您可以将其存储为类变量并从字段 default_factory 返回(如果它是像 list
这样的可变类型,数据类不会让您直接将其指定为默认值)。 对于像 str
和 int
这样的类型,不能保证按预期工作,除非您使用“change_me”值,您知道不会是该字段的合法显式值。
from dataclasses import dataclass, field
from typing import ClassVar, List
@dataclass
class ClassWithState:
name: str
__uninitialized_items: ClassVar[List[str]] = list()
items: List[str] = field(default_factory=lambda: ClassWithState.__uninitialized_items)
def __post_init__(self) -> None:
if self.items is self.__uninitialized_items:
self.items = [str(i) for i in range(len(self.name))]
print(ClassWithState("testing", ["one", "two", "three"]))
print(ClassWithState("testing"))
print(ClassWithState("testing", []))
输出:
ClassWithState(name='testing', items=['one', 'two', 'three'])
ClassWithState(name='testing', items=['0', '1', '2', '3', '4', '5', '6'])
ClassWithState(name='testing', items=[])
如果您不需要通过名称传递显式初始化(或者即使您可以简单地让参数的名称与断言非 None 时使用的名称略有不同),那么 properties 是一个更灵活的选项。 这个想法是让Optional字段成为一个单独的(甚至可能是“私有”)成员,同时让属性可以访问自动转换的版本。 我遇到了这个解决方案,用于解决每次访问对象时都需要应用额外转换的情况,并且强制转换只是一种特殊情况(使属性只读的能力也很好)。 (如果对象引用永远不会改变,你可以考虑
cached_property
。)
这是一个例子:
from dataclasses import dataclass
from typing import List, Optional, cast
@dataclass
class ClassWithState:
name: str
_items: Optional[List[str]] = None
@property
def items(self) -> List[str]:
return cast(List[str], self._items)
@items.setter
def items(self, value: List[str]) -> None:
self._items = value
def __post_init__(self) -> None:
if self._items is None:
self._items = [str(i) for i in range(len(self.name))]
print(ClassWithState("testing", _items=["one", "two", "three"]))
print(ClassWithState("testing", ["one", "two", "three"]))
print(ClassWithState("testing", []))
print(ClassWithState("testing"))
obj = ClassWithState("testing")
print(obj)
obj.items.append('test')
print(obj)
obj.items = ['another', 'one']
print(obj)
print(obj.items)
输出:
ClassWithState(name='testing', _items=['one', 'two', 'three'])
ClassWithState(name='testing', _items=['one', 'two', 'three'])
ClassWithState(name='testing', _items=[])
ClassWithState(name='testing', _items=['0', '1', '2', '3', '4', '5', '6'])
ClassWithState(name='testing', _items=['0', '1', '2', '3', '4', '5', '6'])
ClassWithState(name='testing', _items=['0', '1', '2', '3', '4', '5', '6', 'test'])
ClassWithState(name='testing', _items=['another', 'one'])
['another', 'one']
InitVar[Optional[...]]
字段并使用 __post_init__
设置真实字段如果您可以处理不同的名称,另一种选择是使用
InitVar
指定可选版本只是 __init__
(和 __post_init__
)的参数,然后在其中设置不同的非可选成员变量__post_init__
。这避免了需要进行任何转换,不需要设置属性,允许表示使用目标名称而不是代理名称,并且不会出现没有合理哨兵值的问题,但是,再次,仅当您可以处理与访问字段具有不同名称的初始化参数时,它才有效,并且它不如属性方法灵活:
from dataclasses import InitVar, dataclass, field
from typing import List, Optional
@dataclass
class ClassWithState:
name: str
_items: InitVar[Optional[List[str]]] = None
items: List[str] = field(init=False, default_factory=list)
def __post_init__(self, items: Optional[List[str]]) -> None:
if items is None:
items = [str(i) for i in range(len(self.name))]
self.items = items
用法与属性方法相同,输出也看起来相同,只是表示形式在
items
前面没有下划线。