San*_*tam 5 python immutability python-dataclasses
我遇到一种情况,我希望能够将冻结的dataclass实例视为始终拥有最新数据。或者换句话说,我希望能够检测数据类实例是否已replace调用它并抛出异常。它还应该仅适用于该特定实例,以便相同类型的其他数据类实例的创建/替换不会相互影响。
这是一些示例代码:
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class AlwaysFreshData:
fresh_data: str
def attempt_to_read_stale_data():
original = AlwaysFreshData(fresh_data="fresh")
unaffected = AlwaysFreshData(fresh_data="not affected")
print(original.fresh_data)
new = replace(original, fresh_data="even fresher")
print(original.fresh_data) # I want this to trigger an exception now
print(new.fresh_data)
Run Code Online (Sandbox Code Playgroud)
这里的想法是防止意外突变和从我们的数据类对象中读取过时的内容,以防止错误。
可以这样做吗?通过基类还是其他方法?
编辑:这里的目的是有一种强制/验证数据类的“所有权”语义的方法,即使它只是在运行时。
这是常规数据类有问题的情况的具体示例。
@dataclass
class MutableData:
my_string: str
def sneaky_modify_data(data: MutableData) -> None:
some_side_effect(data)
data.my_string = "something else" # Sneaky string modification
x = MutableData(my_string="hello")
sneaky_modify_data(x)
assert x.my_string == "hello" # as a caller of 'sneaky_modify_data', I don't expect that x.my_string would have changed!
Run Code Online (Sandbox Code Playgroud)
这可以通过使用冻结数据类来防止!但仍然存在可能导致潜在错误的情况,如下所示。
@dataclass(frozen=True)
class FrozenData:
my_string: str
def modify_frozen_data(data: FrozenData) -> FrozenData:
some_side_effect(data)
return replace(data, my_string="something else")
x = FrozenData(my_string="hello")
y = modify_frozen_data(x)
some_other_function(x) # AHH! I probably wanted to use y here instead, since it was modified!
Run Code Online (Sandbox Code Playgroud)
总之,我希望能够防止对数据进行偷偷摸摸或未知的修改,同时强制已替换的数据失效。这可以防止意外使用过时的数据。
这种情况对于某些人来说可能很熟悉,因为它类似于 Rust 中的所有权语义。
至于我的具体情况,我已经有大量使用这些语义的代码,除了NamedTuple实例之外。这是可行的,因为修改_replace任何实例上的函数都可以使实例无效。同样的策略对于数据类来说并不那么干净,因为它dataclasses.replace不是实例本身的函数。
我同意乔恩的观点,即保留适当的数据清单并更新共享实例将是解决问题的更好方法,但如果由于某种原因这是不可能或不可行的(您应该认真检查是否是这样)真的很重要),有一种方法可以实现你所描述的(顺便说一句,很好的模型)。不过,它需要一些不平凡的代码,并且之后对您的数据类有一些限制:
from dataclasses import dataclass, replace, field
from typing import Any, ClassVar
@dataclass(frozen=True)
class AlwaysFreshData:
#: sentinel that is used to mark stale instances
STALE: ClassVar = object()
fresh_data: str
#: private staleness indicator for this instance
_freshness: Any = field(default=None, repr=False)
def __post_init__(self):
"""Updates a donor instance to be stale now."""
if self._freshness is None:
# is a fresh instance
pass
elif self._freshness is self.STALE:
# this case probably leads to inconsistent data, maybe raise an error?
print(f'Warning: Building new {type(self)} instance from stale data - '
f'is that really what you want?')
elif isinstance(self._freshnes, type(self)):
# is a fresh instance from an older, now stale instance
object.__setattr__(self._freshness, '_instance_freshness', self.STALE)
else:
raise ValueError("Don't mess with private attributes!")
object.__setattr__(self, '_instance_freshness', self)
def __getattribute__(self, name):
if object.__getattribute__(self, '_instance_freshness') is self.STALE:
raise RuntimeError('Instance went stale!')
return object.__getattribute__(self, name)
Run Code Online (Sandbox Code Playgroud)
对于您的测试代码来说,其行为如下:
# basic functionality
>>> original = AlwaysFreshData(fresh_data="fresh")
>>> original.fresh_data
fresh
>>> new = replace(original, fresh_data="even fresher")
>>> new.fresh_data
even_fresher
# if fresher data was used, the old instance is "disabled"
>>> original.fresh_data
Traceback (most recent call last):
File [...] in __getattribute__
raise RuntimeError('Instance went stale!')
RuntimeError: Instance went stale!
# defining a new, unrelated instance doesn't mess with existing ones
>>> runner_up = AlwaysFreshData(fresh_data="different freshness")
>>> runner_up.fresh_data
different freshness
>>> new.fresh_data # still fresh
even_fresher
>>> original.fresh_data # still stale
Traceback (most recent call last):
File [...] in __getattribute__
raise RuntimeError('Instance went stale!')
RuntimeError: Instance went stale!
Run Code Online (Sandbox Code Playgroud)
需要注意的一件重要事情是,这种方法向数据类引入了一个新字段,即_freshness,它可能会手动设置并弄乱整个逻辑。您可以尝试在 中捕获它__post_init__,但是类似这样的事情将是让旧实例保持新鲜的有效偷偷摸摸的方法:
>>> original = AlwaysFreshData(fresh_data="fresh")
# calling replace with _freshness=None is a no-no, but we can't prohibit it
>>> new = replace(original, fresh_data="even fresher", _freshness=None)
>>> original.fresh_data
fresh
>>> new.fresh_data
even_fresher
Run Code Online (Sandbox Code Playgroud)
此外,我们需要它的默认值,这意味着在它下面声明的任何字段也需要一个默认值(这还不错 - 只需声明它上面的那些字段),包括来自未来子项的所有字段(这更多的是一个问题,并且有一篇关于如何处理这种情况的大量帖子)。
每当您使用这种模式时,您还需要一个可用的哨兵值。这并不是很糟糕,但对某些人来说可能是一个奇怪的概念。