I have a class, for example Circle
, which has dependent attributes, radius
and circumference
. It makes sense to use a dataclass<
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.