在 __post_init__ 中设置可选数据类参数时如何避免检查 None

问题描述 投票:0回答:2

考虑一个参数具有可变默认值的数据类。为了能够使用新的默认值实例化一个对象而不是共享可变对象,我们可以这样做:

@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?

python python-typing mypy python-dataclasses
2个回答
5
投票

我将

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=[])

4
投票

问题(如果我们需要更多灵活性怎么办?)

问题是我们没有任何方法告诉 mypy

items
__post_init__
之前是可选的,但之后不是。

Carcigenicate 的很好的答案 处理所需的默认初始化不依赖于初始化器的其他参数的情况。但是,假设您需要查看

name
才能了解如何默认初始化
items

对于这种情况,如果有一个与

default_factory
方法类似的方法,将部分初始化的对象的参数作为参数,那就太好了,但不幸的是 没有这样的模拟。其他看起来相关但没有达到目的的事情:

  • init=False
    字段选项允许在
    __post_init__
    中初始化字段,但删除用户指定显式值的选项。
  • 使用
    InitVar
    泛型类型与我们想要的相反:使值可用于初始值设定项(和
    __post_init__
    ),而不将其包含为数据类对象的字段。

使用非 None sentinel

但是,作为解决方法,您可以指定一个特殊的对象值来向

__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
前面没有下划线。

© www.soinside.com 2019 - 2024. All rights reserved.