Indeed, there is a way!
Code
from dataclasses import dataclass, field
@dataclass
class Data:
_name: str | None = None
file_friendly_name: str | None = field(default=None, init=False)
def __post_init__(self):
# If _name is not None, automatically create the file_friendly_name
if self._name is not None:
self.file_friendly_name = "".join(
i for i in self._name if i not in "/:*?<>|"
)
@property
def name(self) -> str | None:
return self._name
@name.setter
def name(self, new_val: str | None) -> None:
if self._name == new_val:
return
self._name = new_val
if self._name is None:
self.file_friendly_name = None
else:
self.file_friendly_name = "".join(
i for i in self._name if i not in "/:*?<>|"
)
Explanation
Since you asked for a way to actually update the file_friendly_name
field whenever name
changes, I've changed the name
field into a property which reads from private attribute _name
. Now it's _name
which is assessed in __post_init__
.
Then I've created a "setter" for the name
property. This setter will be called every time name
is updated. Note that there's no sort of Data.name.setattr(...)
boilerplate-y nonsense, given that we're in Python-land. When I say "updated", I mean whenever you do
>>> d = Data("Zev")
>>> d.name = "**Zev**"
that setter will be invoked and the name
and file_friendly_name
fields will be updated accordingly:
>>> d.file_friendly_name
'Zev'
>>> d.name
'**Zev**'
>>> data = Data()
>>> data.name = 'foo/bar'
>>> print(data.file_friendly_name)
'foobar'
>>> data = Data('foo/bar')
>>> data.name = 'new?name'
>>> print(data.file_friendly_name)
'newname'
Pretty Printing
One small drawback of this is that printing data
shows our private field:
>>> print(data)
Data(_name='new?name', file_friendly_name='newname')
However, you can work around this by defining your own __repr__
method:
def __repr__(self) -> str:
return f"Data(name='{self._name}', file_friendly_name='{self.file_friendly_name}')"
>>> print(data)
Data(name='new?name', file_friendly_name='newname')
Making name
work in the constructor
Finally, if you'd like your name
keyword argument back for constructing Data
instances, you can add your own constructor to it. We'll DRY up the code this requires while we're at it:
def __init__(self, name: str | None = None):
self._name = name
if self._name is not None:
self.file_friendly_name = self.make_file_friendly_name(self._name)
def __post_init__(self):
# If _name is not None, automatically create the file_friendly_name
if self._name is not None:
self.file_friendly_name = self.make_file_friendly_name(self._name)
@name.setter
def name(self, new_val: str | None) -> None:
if self._name == new_val:
return
self._name = new_val
if self._name is None:
self.file_friendly_name = None
else:
self.file_friendly_name = self.make_file_friendly_name(self._name) # revised
@staticmethod
def make_file_friendly_name(name: str) -> str:
return "".join(
i for i in name if i not in "\\/:*?<>|"
)
After this, the sample code works as expected:
>>> data = Data()
>>> data.name = 'foo/bar'
>>> print(data.file_friendly_name)
'foobar'
>>> data = Data(name='foo/bar')
>>> data.name = 'new?name'
>>> print(data.file_friendly_name)
'newname'