Python 冻结数据类,允许通过方法更改属性

Python frozen dataclass, allow changing of attribute via method

假设我有一个数据类:

@dataclass(frozen=True)
class Foo:
    id: str
    name: str

我希望它是不可变的(因此 frozen=True),这样 foo.id = barfoo.name = baz 就会失败。但是,我希望能够去掉 id,像这样:

foo = Foo(id=10, name="spam")

foo.strip_id()
foo
-> Foo(id=None, name="spam")

我尝试了一些方法,重写 setattr,但没有任何效果。有一个优雅的解决方案吗? (我知道我可以写一个方法 returns 一个新的冻结实例,除了那个 id 被删除之外是相同的,但这看起来有点 hacky,它需要我做 foo = foo.strip_id(),因为foo.strip_id() 实际上不会改变 foo)

编辑:

虽然有些评论者似乎不同意,但我认为 'fully mutable, do what you want with it' 和 'immutable, except in this particular, tightly controlled way'

之间存在合理的区别

好吧,你可以通过直接修改实例的__dict__成员使用object.__setattr__(...)1[修改属性来做到这一点=41=],但为什么呢???专门询问 immutable 然后使其可变是......优柔寡断。但如果你必须:

from dataclasses import dataclass

@dataclass(frozen=True)
class Foo:
    id: str
    name: str
    def strip_id(self):
        object.__setattr__(self, 'id', None)

foo=Foo(10, 'bar')

>>> foo
Foo(id=10, name='bar')
>>> foo.strip_id()
>>> foo
Foo(id=None, name='bar')

任何执行此操作的方法都可能看起来很老套...因为它需要做的事情从根本上与设计相反。

如果您将其用作向其他程序员发出信号,告诉他们他们不应修改这些值,通常在 Python 中使用的方法是在变量名前加上一个下划线作为前缀。如果你想这样做,同时也使值可访问,Python 有一个名为 property 的内置模块,其中(来自文档)“典型用途是定义一个托管属性”:

from dataclasses import dataclass

@dataclass
class Foo:
    _name: str
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, value):
        self._name = value
    @name.deleter
    def name(self):
        self._name = None

那么你可以这样使用它:

>>> f=Foo()
>>> f.name = "bar"
>>> f.name
'bar'
>>> f._name
'bar'
>>> del f.name
>>> f.name
>>> f._name

装饰方法将 _name 的实际值隐藏在 name 后面,以控制用户如何与该值交互。您可以使用它在存储或返回数据之前将转换规则或验证检查应用于数据。

这与使用 @dataclass(frozen=True) 的效果不尽相同,如果您尝试将其声明为已冻结,则会出现错误。将冻结数据类与 属性 装饰器混合并不简单,我还没有看到一个令人满意的简洁直观的解决方案。 @Arne 在 GitHub 上发布了 this answer, and I found this thread,但这两种方法都不是很鼓舞人心;如果我在必须维护的代码中遇到这样的事情,我不会很高兴(但我 感到困惑,并且可能会非常恼火)。


1:根据@Arne 的回答修改,他观察到内部使用字典作为数据容器是不保证的。

作为对 Z4-tier 解决方案的轻微改进,请使用 object.__setattr__ 而不是 self.__dict__ 来操作冻结数据类的属性。 类 使用字典来存储它们的属性的事实只是默认行为,特别是 data类 会经常使用 __slots__ 来代替,因为它减少了内存占用。

from dataclasses import dataclass

@dataclass(frozen=True)
class Foo:
    id: str
    name: str

    def strip_id(self):
        object.__setattr__(self, 'a', None)   

而不是这个 object.__setattr__(self, 'id', None) hack-alike,在 __init__ 方法中尤其丑陋,难道不能有一个可选的标记值(例如 NOTSETpytest), 这将被允许修改为字段类型的具体值,只有一次? 这将非常容易实现,并允许具有不可变性的灵活性 once-set.