问题
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__
, ...).
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)
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__
.
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?
回答1:
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)
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
.
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)
In my opinion, freezing should only happen after the __init__
by default, but for now I will probably use the first approach.
来源:https://stackoverflow.com/questions/57791679/using-dataclasses-with-dependent-attributes-via-property