使用dataclasses.MISSING作为Python数据类的可选参数值?

x8diyxa7  于 2023-03-28  发布在  Python
关注(0)|答案(2)|浏览(98)

我想让set参数可选,但仍然允许None是一个有效值。根据文档,它建议可以使用dataclasses.MISSING作为默认值来帮助实现这一点。
如上所示,MISSING值是一个sentinel对象,用于检测用户是否提供了某些参数。使用此sentinel是因为None是某些具有不同含义的参数的有效值。任何代码都不应直接使用MISSING值。
但是通过如下方式使用它:

import dataclasses
from dataclasses import dataclass, field

@dataclass
class Var:
    get: list
    set: list = dataclasses.MISSING

    def __post_init__(self):
        if self.set is dataclasses.MISSING:
            self.set = self.get

 print(Var(get=['Title']))

我收到一个错误:

Traceback (most recent call last):
File "main.py", line 31, in <module>
print(Var(get=['Title']))
TypeError: __init__() missing 1 required positional argument: 'set'
xtfmy6hx

xtfmy6hx1#

代码不能直接使用MISSING值。

上面这部分在文档中被注意到是有原因的。因此,如果可能的话,我们应该避免在应用程序代码中使用MISSING用法(和导入)。在这种情况下,使用MISSING根本不适用于我们的用例。
假设用法(避免直接使用MISSING sentinel值,而是使用dataclasses.field(...)

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class Var:
    get: list[str]
    set: Optional[list[str]] = field(default=None)

print(Var(get=['Title']))
# Var(get=['Title'], set=None)
但是MISSING实际用在哪里呢?

MISSING是一个sentinel对象,dataclasses模块在后台使用它的魔力。
您可以查看dataclasses.field的源代码,并在那里找到它的明确用法:

def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
          hash=None, compare=True, metadata=None):

你会看到像default这样的字段声明的默认值是default=MISSING而不是default=None。这样做主要是为了确定用户是否真的将defaultdefault_factory的值传递给工厂函数fields。例如,像我们在上面的例子中所做的那样传递field(default=None)是完全有效的;但是,由于默认值实际上是MISSING,因此dataclasses能够检测到已为此参数传递了一个值(值None)。

MISSING如何声明

如果你检查dataclasses模块的源代码,或者通过Ctrl(在Mac上是Command)+左击代码中任何地方的关键字MISSING,你可以看到MISSING实际上是如何声明的:

# A sentinel object to detect if a parameter is supplied or not.  Use
# a class to give it a better repr.
class _MISSING_TYPE:
    pass
MISSING = _MISSING_TYPE()
一种可能的解决方案

借助dataclasses模块定义MISSING的方式,理论上您可以定义自己的(空)类,然后示例化该类。
然而,我觉得在这种情况下可以避免类示例化。下面是一行代码来创建一个sentinel类/类型:

class _UNSET: ...  # here the ellipsis (`...`) is essentially the same as `pass`

那么,用法如下:

from __future__ import annotations

from dataclasses import dataclass

class _UNSET: ...

@dataclass
class Var:
    get: list
    set: list | None = _UNSET

    def __post_init__(self):
        if self.set is _UNSET:
            self.set = self.get

print(Var(get=[7]))
print(Var(get=[7], set=[8]))
print(Var(get=[7], set=None))

这可以正确区分在构造函数中省略set或指定set的值(如set=None)的情况。
结果也如预期:

Var(get=[7], set=[7])
Var(get=[7], set=[8])
Var(get=[7], set=None)
wlsrxk51

wlsrxk512#

我不知道你是否可以用这种方式使用dataclasses.MISSING,所以我会简单地使用一个专用的enum。因为它是一个enum,它被保证只与它自己相同,所以它应该给予你想要的用途:

from dataclasses import dataclass
from enum import Enum

_field_status = Enum("FieldStatus", "UNSET")

@dataclass
class Var:
    get: list
    set: list = _field_status.UNSET

    def __post_init__(self):
        if self.set is _field_status.UNSET:
            self.set = self.get

print(Var(get = [7]))

print(Var(get=[7], set=[8]))
print(Var(get=[7], set=None))

显然,这会阻止用户将set设置为_field_status.UNSET,但大概他们不需要这样做。
请注意,我有点困惑,为什么None是一个有效的值,它被暗示为一个 * 列表 *,但原则是成立的。

相关问题