Reputation: 1310
I have a dataclass that looks like this
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, automatically create the file_friendly_name
if self.name:
self.file_friendly_name = "".join(
i for i in self.name if i not in "/:*?<>|"
)
If user passes name
on instantiation, file_friendly_name
is automatically created.
Is there a way to do it so that every time name
is updated/changed, file_friendly_name
also changes?
e.g.
data = Data()
data.name = 'foo/bar'
print(data.file_friendly_name) # want: 'foobar'
data = Data(name='foo/bar')
data.name = 'new?name'
print(data.file_friendly_name) # want: 'newname'
Update based on answers:
_name: str
and creating name
using getters/setters. But I don't like how when you do print(Data())
it shows _name
as an attribute. I'd like that not to happen.file_friendly_name
as a property. But then you can't see that as an attribute when you do print(Data())
. This is less of an issue but still not ideal.Can it just show name
and file_friendly_name
as attributes when doing print(Data())
?
Upvotes: 2
Views: 635
Reputation: 12140
I'd suggest defining file_friendly_name
as @property
instead.
from dataclasses import dataclass, fields
@dataclass
class Data:
name: str | None = None
@property
def file_friendly_name(self) -> str | None:
if self.name is not None:
return "".join(
i for i in self.name if i not in "\/:*?<>|"
)
else:
return None
def __repr__(self):
fields_str = [f'{field.name}={getattr(self, field.name)!r}'
for field in fields(self)]
fields_str.append(f'file_friendly_name={self.file_friendly_name}')
fields_res = ', '.join(fields_str)
return f'{type(self).__name__}({fields_res})'
Upvotes: 4
Reputation: 1134
Indeed, there is a way!
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 "/:*?<>|"
)
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'
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')
name
work in the constructorFinally, 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'
Upvotes: 3
Reputation: 7971
Similiarly to @Yevhen's suggestion, but using setter on property you can trigger a specific function when setting to an attribute. You can then check if class has related private attribute to tell if you are definining it right now or it already exists.
from dataclasses import dataclass, field
def methodToTrigger():
print("Triggered method")
@dataclass
class Data:
name: str = None
def __post_init__(self):
# If name, automatically create the file_friendly_name
if self.name:
self.file_friendly_name = "".join(
i for i in self.name if i not in "\/:*?<>|"
)
@property
def file_friendly_name(self):
return self._file_friendly_name
@file_friendly_name.setter
def file_friendly_name(self, value):
if not hasattr(self, "_file_friendly_name"):
methodToTrigger()
self._file_friendly_name = value
d = Data(name = "asdf")
print(d.file_friendly_name)
Upvotes: 2