是否可以防止从冻结的 python 数据类中读取数据?
Is it possible to prevent reading from a frozen python dataclass?
我有一种情况,我希望能够将冻结的 dataclass
实例视为始终具有最新数据。或者换句话说,我希望能够检测到 dataclass 实例是否已调用 replace
并抛出异常。它也应该仅适用于该特定实例,以便 creation/replacements 的其他数据class 相同类型的实例不会相互影响。
下面是一些示例代码:
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)
这里的想法是防止 意外突变和从我们的数据class 对象中读取过时数据以防止错误。
可以这样做吗?通过基础 class 或其他方法?
编辑:此处的目的是为数据classes 提供一种enforcing/verifying“所有权”语义,即使它仅在运行时。
这是一个有问题的常规数据class情况的具体示例。
@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!
这可以通过使用冻结数据来避免classes!但是仍然有一种情况会导致潜在的错误,如下所示。
@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!
总而言之,我希望能够防止 对数据进行偷偷摸摸或未知的修改,同时强制使已替换的数据失效。
这样可以防止意外使用过时的数据。
有些人可能很熟悉这种情况,因为它类似于 Rust 中的所有权语义。
至于我的具体情况,我已经有大量使用这些语义的代码,除了 NamedTuple
个实例。这是可行的,因为在任何实例上修改 _replace
函数都允许使实例无效。同样的策略对于数据classes 并不适用,因为dataclasses.replace
不是实例本身的函数。
我同意 Jon 的观点,保持适当的数据清单和更新共享实例将是解决问题的更好方法,但如果由于某种原因这不可能或不可行(你应该认真检查它是否 真的 足够重要),有一种方法可以实现你所描述的(顺便说一句,好的模型)。不过,它需要一些重要的代码,并且之后对您的数据类有一些限制:
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)
您的测试代码的行为如下:
# 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!
需要注意的一件重要事情是,这种方法向数据类引入了一个新字段,即 _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
此外,我们需要它的默认值,这意味着在它下面声明的任何字段也需要一个默认值(这还不错 - 只需在它上面声明那些字段),包括未来子项的所有字段(这是一个更大的问题,关于如何处理这种情况有 )。
无论何时使用这种模式,您还需要一个可用的标记值。这并不是很糟糕,但对某些人来说可能是一个奇怪的概念。
我有一种情况,我希望能够将冻结的 dataclass
实例视为始终具有最新数据。或者换句话说,我希望能够检测到 dataclass 实例是否已调用 replace
并抛出异常。它也应该仅适用于该特定实例,以便 creation/replacements 的其他数据class 相同类型的实例不会相互影响。
下面是一些示例代码:
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)
这里的想法是防止 意外突变和从我们的数据class 对象中读取过时数据以防止错误。
可以这样做吗?通过基础 class 或其他方法?
编辑:此处的目的是为数据classes 提供一种enforcing/verifying“所有权”语义,即使它仅在运行时。
这是一个有问题的常规数据class情况的具体示例。
@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!
这可以通过使用冻结数据来避免classes!但是仍然有一种情况会导致潜在的错误,如下所示。
@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!
总而言之,我希望能够防止 对数据进行偷偷摸摸或未知的修改,同时强制使已替换的数据失效。 这样可以防止意外使用过时的数据。
有些人可能很熟悉这种情况,因为它类似于 Rust 中的所有权语义。
至于我的具体情况,我已经有大量使用这些语义的代码,除了 NamedTuple
个实例。这是可行的,因为在任何实例上修改 _replace
函数都允许使实例无效。同样的策略对于数据classes 并不适用,因为dataclasses.replace
不是实例本身的函数。
我同意 Jon 的观点,保持适当的数据清单和更新共享实例将是解决问题的更好方法,但如果由于某种原因这不可能或不可行(你应该认真检查它是否 真的 足够重要),有一种方法可以实现你所描述的(顺便说一句,好的模型)。不过,它需要一些重要的代码,并且之后对您的数据类有一些限制:
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)
您的测试代码的行为如下:
# 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!
需要注意的一件重要事情是,这种方法向数据类引入了一个新字段,即 _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
此外,我们需要它的默认值,这意味着在它下面声明的任何字段也需要一个默认值(这还不错 - 只需在它上面声明那些字段),包括未来子项的所有字段(这是一个更大的问题,关于如何处理这种情况有
无论何时使用这种模式,您还需要一个可用的标记值。这并不是很糟糕,但对某些人来说可能是一个奇怪的概念。