在 __post_init__ 中设置可选数据类参数时如何避免检查 None
How to avoid checking for None when setting Optional dataclass args in __post_init__
考虑一个参数具有可变默认值的数据class。为了能够使用新的默认值而不是共享可变对象来实例化一个对象,我们可以这样做:
@dataclass
class ClassWithState:
name: str
items: Optional[List[str]] = None
def __post_init__(self) -> None:
if self.items is None:
self.items = []
这按预期工作。但是,每当我在这个 class 的某些实例中引用 items
时,mypy 会警告 items
可能是 None。例如:
c = ClassWithState("object name")
c.items.append("item1")
MyPy 会抱怨:
Item "None" of "Optional[List[str]]" has no attribute "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
的方法,将部分初始化对象的参数作为参数接收,那就太好了,但不幸的是 there is no such analog。其他可能看起来相关但不符合目的的东西:
init=False
字段选项,允许在 __post_init__
中初始化字段,但删除用户指定显式值的选项。
- 使用
InitVar
泛型与我们在这里想要的相反:使值对初始化器(和 __post_init__
)可用,而不将其作为数据字段包含在内class对象。
使用非None 标记值
但是,作为变通方法,您可以指定一个特殊的对象值来向 __post_init__
方法表示字段的默认值需要被替换。对于大多数类型,很容易只创建一个特定类型的唯一虚拟对象,您可以将其存储为 class 变量和 return 来自字段 default_factory (如果它是可变的type like list
, dataclass 不会让你直接赋给它作为默认值)。对于像 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=[])
如果字段的名称可以略有不同...
使用属性
如果 你不需要通过名称传递显式初始化(或者即使你可以简单地让参数的名称与断言非[时使用的名称略有不同) =84=]),那么 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
前面有下划线。
考虑一个参数具有可变默认值的数据class。为了能够使用新的默认值而不是共享可变对象来实例化一个对象,我们可以这样做:
@dataclass
class ClassWithState:
name: str
items: Optional[List[str]] = None
def __post_init__(self) -> None:
if self.items is None:
self.items = []
这按预期工作。但是,每当我在这个 class 的某些实例中引用 items
时,mypy 会警告 items
可能是 None。例如:
c = ClassWithState("object name")
c.items.append("item1")
MyPy 会抱怨:
Item "None" of "Optional[List[str]]" has no attribute "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
的方法,将部分初始化对象的参数作为参数接收,那就太好了,但不幸的是 there is no such analog。其他可能看起来相关但不符合目的的东西:
init=False
字段选项,允许在__post_init__
中初始化字段,但删除用户指定显式值的选项。- 使用
InitVar
泛型与我们在这里想要的相反:使值对初始化器(和__post_init__
)可用,而不将其作为数据字段包含在内class对象。
使用非None 标记值
但是,作为变通方法,您可以指定一个特殊的对象值来向 __post_init__
方法表示字段的默认值需要被替换。对于大多数类型,很容易只创建一个特定类型的唯一虚拟对象,您可以将其存储为 class 变量和 return 来自字段 default_factory (如果它是可变的type like list
, dataclass 不会让你直接赋给它作为默认值)。对于像 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=[])
如果字段的名称可以略有不同...
使用属性
如果 你不需要通过名称传递显式初始化(或者即使你可以简单地让参数的名称与断言非[时使用的名称略有不同) =84=]),那么 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
前面有下划线。