微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在__post_init__中设置可选数据类参数时,如何避免检查None 问题如果我们需要更多灵活性怎么办?使用非 None sentinel 值如果字段可以有一个稍微不同的名称......

如何解决在__post_init__中设置可选数据类参数时,如何避免检查None 问题如果我们需要更多灵活性怎么办?使用非 None sentinel 值如果字段可以有一个稍微不同的名称......

考虑一个具有可变认值的数据类作为参数。为了能够实例化具有新的认值而不是共享的可变对象的对象,我们可以执行以下操作:

@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]]”的项目“无”没有属性“附加”。

我不想每次引用items时都需要添加不必要的检查,例如

assert c.items is not None

我到处引用的items。我怎么能说服mypy items永远不会是None?

解决方法

我会在设置default_factory选项的情况下使用field

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__ 之前是 Optional 而不是之后。

Carcigenicate 的 nice answer 处理所需的默认初始化不依赖于初始化程序的其他参数的情况。但是,假设您需要查看 name 以了解如何默认初始化 items

对于这种情况,如果有一个类似于 default_factory 的方法将部分初始化的对象的参数作为参数,那就太好了,但不幸的是 there is no such analog。其他看起来相关但不符合目的的内容:

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

使用非 None sentinel

但是,作为一种变通方法,您可以指定一个特殊的对象值来表示需要替换字段默认值的 __post_init__ 方法。对于大多数类型,很容易创建特定类型的唯一虚拟对象,您可以将其存储为类变量并从字段 default_factory 返回(如果它是像 list 这样的可变类型,则数据类不会让您直接将其指定为默认值)。对于 strint 之类的类型,不能保证按预期工作,除非您使用“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=[])

如果字段可以有一个稍微不同的名称......

使用属性

如果您不需要按名称传递显式初始化(​​或者即使您可以简单地让参数的名称与在断言非无时使用您的名称略有不同),则 {{ 3}} 是一个更灵活的选项。 这个想法是让 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","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',_items=[])
ClassWithState(name='testing',_items=['0','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 前没有下划线。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。