通过属性使用具有依赖属性的数据类 [英] Using dataclasses with dependent attributes via property
问题描述
我有一个类,例如Circle
,它具有相关属性radius
和circumference
.在这里使用dataclass
是有意义的,因为__init__
,__eq__
,__repr__
的样板和排序方法(__lt__
,...).
I have a class, for example Circle
, which has dependent attributes, radius
and circumference
. It makes sense to use a dataclass
here because of the boilerplate for __init__
, __eq__
, __repr__
and the ordering methods (__lt__
, ...).
我选择其中一个属性来依赖另一个属性,例如圆周是根据半径计算得出的.由于该类应支持使用两个属性之一进行初始化(+将它们包含在__repr__
和dataclasses.asdict
中),因此我都对这两个注释了:
I choose one of the attributes to be dependent on the other, e.g. the circumference is computed from the radius. Since the class should support initialization with either of the attributes (+ have them included in __repr__
as well as dataclasses.asdict
) I annotate both:
from dataclasses import dataclass
import math
@dataclass
class Circle:
radius: float = None
circumference: float = None
@property
def circumference(self):
return 2 * math.pi * self.radius
@circumference.setter
def circumference(self, val):
if val is not type(self).circumference: # <-- awkward check
self.radius = val / (2 * math.pi)
这需要我为if val is not type(self).circumference
添加一些笨拙的检查,因为如果没有为__init__
提供任何值,则设置者将收到此检查.
This requires me to add the somewhat awkward check for if val is not type(self).circumference
because this is what the setter will receive if no value is provided to __init__
.
然后,如果我想通过声明frozen=True
使该类可哈希化,则需要将self.radius = ...
更改为object.__setattr__(self, 'radius', ...)
,因为否则,它将尝试分配给冻结实例的字段.
Then if I wanted to make the class hashable by declaring frozen=True
I need to change self.radius = ...
to object.__setattr__(self, 'radius', ...)
because otherwise this would attempt to assign to a field of a frozen instance.
所以我的问题是,这是同时使用数据类和属性的明智方式,还是潜在的(非显而易见的)障碍摆在眼前,在这种情况下我应该避免使用数据类?也许还有更好的方法可以实现这一目标?
So my question is if this is a sane way of using dataclasses together with properties or if potential (non-obvious) obstacles lie ahead and I should refrain from using dataclasses in such cases? Or maybe there is even a better way of achieving this goal?
推荐答案
对于初学者,您可以在__init__
方法中设置属性,如下所示:
For starters, you could set the attributes in the __init__
method as follows:
from dataclasses import dataclass, InitVar
import math
@dataclass(frozen=True, order=True)
class CircleWithFrozenDataclass:
radius: float = 0
circumference: float = 0
def __init__(self, radius=0, circumference=0):
super().__init__()
if circumference:
object.__setattr__(self, 'circumference', circumference)
object.__setattr__(self, 'radius', circumference / (2 * math.pi))
if radius:
object.__setattr__(self, 'radius', radius)
object.__setattr__(self, 'circumference', 2 * math.pi * radius)
这仍将为您提供所有有用的__eq__
,__repr__
,__hash__
和订购方法注入.尽管object.__setattr__
看起来很丑,但请注意 CPython实现在为冻结的dataclass
注入生成的__init__
方法时,它本身使用object.__setattr__
设置属性.
This will still provide you with all the helpful __eq__
, __repr__
, __hash__
, and ordering method injections. While object.__setattr__
looks ugly, note that the CPython implementation itself uses object.__setattr__
to set attributes when injecting the generated __init__
method for a frozen dataclass
.
如果您真的想摆脱object.__setattr__
,则可以设置frozen=False
(默认值)并自己覆盖__setattr__
方法.这是在复制数据类的frozen
功能如何在CPython中实现.请注意,由于frozen=False
之后不再插入__hash__
,因此您还必须打开unsafe_hash=True
.
If you really want to get rid of object.__setattr__
, you can set frozen=False
(the default) and override the __setattr__
method yourself. This is copying how the frozen
feature of dataclasses is implemented in CPython. Note that you will also have to turn on unsafe_hash=True
as __hash__
is no longer injected since frozen=False
.
@dataclass(unsafe_hash=True, order=True)
class CircleUsingDataclass:
radius: float = 0
circumference: float = 0
_initialized: InitVar[bool] = False
def __init__(self, radius=0, circumference=0):
super().__init__()
if circumference:
self.circumference = circumference
self.radius = circumference / (2 * math.pi)
if radius:
self.radius = radius
self.circumference = 2 * math.pi * radius
self._initialized = True
def __setattr__(self, name, value):
if self._initialized and \
(type(self) is __class__ or name in ['radius', 'circumference']):
raise AttributeError(f"cannot assign to field {name!r}")
super().__setattr__(name, value)
def __delattr__(self, name, value):
if self._initialized and \
(type(self) is __class__ or name in ['radius', 'circumference']):
raise AttributeError(f"cannot delete field {name!r}")
super().__delattr__(name, value)
我认为默认情况下,冻结仅应在__init__
之后进行,但现在我可能会使用第一种方法.
In my opinion, freezing should only happen after the __init__
by default, but for now I will probably use the first approach.
这篇关于通过属性使用具有依赖属性的数据类的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!