当Frozen = True时,如何在__post_init__中设置dataclass字段的值?

nic*_*hen 20 python

我正在尝试创建冻结的数据类,但在设置中的值时遇到问题__post_init__。有没有办法从设置基于值的字段值init paramdataclass使用时frozen=True设置?

RANKS = '2,3,4,5,6,7,8,9,10,J,Q,K,A'.split(',')
SUITS = 'H,D,C,S'.split(',')


@dataclass(order=True, frozen=True)
class Card:
    rank: str = field(compare=False)
    suit: str = field(compare=False)
    value: int = field(init=False)
    def __post_init__(self):
        self.value = RANKS.index(self.rank) + 1
    def __add__(self, other):
        if isinstance(other, Card):
            return self.value + other.value
        return self.value + other
    def __str__(self):
        return f'{self.rank} of {self.suit}'
Run Code Online (Sandbox Code Playgroud)

这就是痕迹

 File "C:/Users/user/.PyCharm2018.3/config/scratches/scratch_5.py", line 17, in __post_init__
    self.value = RANKS.index(self.rank) + 1
  File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'value'
Run Code Online (Sandbox Code Playgroud)

use*_*ica 18

使用生成的__init__方法做的同样的事情object.__setattr__

def __post_init__(self):
    object.__setattr__(self, 'value', RANKS.index(self.rank) + 1)
Run Code Online (Sandbox Code Playgroud)

  • 这可行。然而,似乎当从`__post_init__`中以具有`init = False`的名称调用时,`dataclass`生成的__setattr__`应该不引发`FrozenInstanceError`。像这样使用`object .__ setattr__`是丑陋/乏味的。 (8认同)
  • `super().__setattr__('attr_name', value)` 对我来说似乎更干净。只要数据类不从另一个冻结数据类继承,就应该有效 (7认同)
  • @xuhdev“这个答案只是遵循文档”:如果我们想成为精确者,则不是真的,相反,这个答案遵循生成的 `__init__` 的作用(在文档中提到)。值得注意的是,文档没有建议 OP 问题的解决方案。 (4认同)
  • @Conchylicultor我同意,但这个答案只是遵循[文档](https://docs.python.org/3/library/dataclasses.html#frozen-instances):`使用 freeze=True 时会有微小的性能损失: __init__() 不能使用简单的赋值来初始化字段,必须使用 object.__setattr__()。` (2认同)
  • @AlirezaMohamadi:与 Java 不同,Python 允许您显式调用特定类的方法实现。在这里,我们从“object”调用“__setattr__”实现,绕过冻结数据类中的覆盖。 (2认同)
  • `object.__dict__['attr_name'] = value` 是错误的,因为它会尝试在 `object` 的 `__dict__` 而不是 `self` 中设置一个条目。`self.__dict__['attr_name'] = value` 适用于大多数属性,但对于任何需要通过 [描述符](https://docs.python.org/3/reference/datamodel. html#implementing-descriptors),例如使用“__slots__”的属性(如果“dataclasses”添加了“__slots__”支持)。 (2认同)
  • &gt; (如果数据类添加了 __slots__ 支持)。@user2357112supportsMonica 插槽支持在几个月前的 Python 3.10 中添加:https://docs.python.org/3/whatsnew/3.10.html#dataclasses (以及最后使数据类继承可用的 kw_only 参数) (2认同)

Pet*_*ler 14

使用缓存属性避免对象突变的解决方案

这是 @Anna Giasson 答案的简化版本。

冻结数据类与functools模块中的缓存配合良好 。dataclass您可以定义一个带注释的方法,而不是使用字段@functools.cached_property,该方法仅在第一次查找属性时进行评估。这是原始示例的最小版本:

from dataclasses import dataclass
import functools

@dataclass(frozen=True)
class Card:
    rank: str

    @functools.cached_property
    def value(self):
        # just for demonstration:
        # this gets printed only once per Card instance
        print("Evaluate value")
        return len(self.rank)

card = Card(rank="foo")

assert card.value == "foo"
assert card.value == "foo"
Run Code Online (Sandbox Code Playgroud)

在实践中,如果评估成本低廉,您也可以使用非缓存@property装饰器。

  • 这似乎是一种更好地与不可变语义保持一致的方法,同时也不会引入低级的不规则 python hacks 或习语。 (3认同)

Max*_*ner 13

我在几乎所有类中使用的解决方案是将附加构造函数定义为类方法。

根据给定的示例,可以将其重写如下:

@dataclass(order=True, frozen=True)
class Card:
    rank: str = field(compare=False)
    suit: str = field(compare=False)
    value: int

    def __post_init__(self) -> None:
        if not is_valid_rank(self.rank):
            raise ValueError(f"Rank {self.rank} of Card is invalid!")

    @classmethod
    def from_rank_and_suite(cls, rank: str, suit: str) -> "Card":
        value = RANKS.index(self.rank) + 1
        return cls(rank=rank, suit=suit, value=value)
Run Code Online (Sandbox Code Playgroud)

这样一来,人们就拥有了所需的所有自由,而不必求助于__setattr__黑客,也不必放弃所需的严格性,例如frozen=True.

  • 我喜欢这种方法!但使用 `__post_init__` 的优点之一是保证不变量。通过`@classmethod`/`@staticmethod`方法,使用数据类的人仍然可以直接构造它。 (4认同)
  • 你是对的,重要的不变量没有被强制执行。然而,如果它们很重要,人们仍然可以实现“__post_init__”来确保这些不变量。 (3认同)

Tim*_*Tim 6

使用突变

不应改变冻结的物体。但有时可能会出现这种需要。接受的答案非常适合这一点。这是解决此问题的另一种方法:返回具有更改后的值的新实例。对于某些情况来说,这可能有点过分了,但它是一种选择。

from copy import deepcopy

@dataclass(frozen=True)
class A:
    a: str = ''
    b: int = 0

    def mutate(self, **options):
        new_config = deepcopy(self.__dict__)
        # some validation here
        new_config.update(options)
        return self.__class__(**new_config)
Run Code Online (Sandbox Code Playgroud)

另一种方法

如果要设置全部或多个值,可以__init__在 内部再次调用__post_init__。虽然用例不多。

以下示例并不实用,仅用于演示可能性。

from dataclasses import dataclass, InitVar


@dataclass(frozen=True)
class A:
    a: str = ''
    b: int = 0
    config: InitVar[dict] = None

    def __post_init__(self, config: dict):
        if config:
            self.__init__(**config)
Run Code Online (Sandbox Code Playgroud)

以下调用

A(config={'a':'a', 'b':1})
Run Code Online (Sandbox Code Playgroud)

将产生

A(a='a', b=1)
Run Code Online (Sandbox Code Playgroud)

没有抛出错误。这是在 python 3.7 和 3.9 上测试的。

当然,你可以直接使用 构造A(a='hi', b=1),但也许还有其他用途,例如从 json 文件加载配置。

奖励:更疯狂的用法

A(config={'a':'a', 'b':1, 'config':{'a':'b'}})
Run Code Online (Sandbox Code Playgroud)

将产生

A(a='b', b=1)
Run Code Online (Sandbox Code Playgroud)

  • [dataclasses.replace](https://docs.python.org/3/library/dataclasses.html#dataclasses.replace) 旨在用于此目的。 (6认同)