Reputation: 5500
Given the following code:
class Base:
__slots__ = 'base_field',
def __init__(self, base_field):
self.base_field = base_field
def __repr__(self):
return f"{self.__class__.__name__}(base_field={self.base_field!r})"
class Derived(Base):
__slots__ = 'derived_field',
def __init__(self, derived_field, **kwargs):
super().__init__(**kwargs)
self.derived_field = derived_field
def __repr__(self):
return f"{self.__class__.__name__}(base_field={self.base_field!r}, derived_field={self.derived_field!r})"
b1 = Base('base')
print(repr(b1))
b2 = eval(repr(b1))
d1 = Derived(base_field='dbase', derived_field='derived')
print(repr(d1))
d2 = eval(repr(d1))
This code will break if I add a new field to Base
and forget to update the Derived
class' __repr__
.
How and where should I define a __repr__
method(s) such that:
Base
and Derived
instances to repr()
, their correct strings will be returned (e.g. repr(Base("base")) == "Base(base_field="base")
and repr(Derived(base_field='dbase', derived_field='derived')) == "Derived(base_field="base", derived_field="derived")
Base
class and those fields will show up in in the Derived
class' __repr__
output. In other words, the Derived
class' __repr__
function should not know about the fields in the Base
class.I'm reading some code where the author has solved the problem by making the Derived
class __repr__
call super().__repr__()
and then remove the last parenthesis and append the Derived
attributes to the string. This does the job but I'm wondering if there is a more Pythonic way of achieving this goal. Or, perhaps this is the Pythonic way?
I looked into Python's dataclass
and that does a great job of building __repr__
s that do the right thing. However they use __dict__
s so there are trade-offs to consider (e.g. smaller memory foot print for an object that has many instances at once, vs. easier __repr__
).
Thank you!
Upvotes: 0
Views: 702
Reputation: 96360
You can do this dynamically, just introspect on slots and make sure to walk the MRO:
class Base:
__slots__ = 'base_field',
def __init__(self, base_field):
self.base_field = base_field
def __repr__(self):
slots = [
slot
for klass in type(self).mro() if hasattr(klass, '__slots__')
for slot in klass.__slots__
]
args = ", ".join(
[f"{slot}={getattr(self, slot)!r}" for slot in reversed(slots)]
)
return f"{type(self).__name__}({args})"
class Derived(Base):
__slots__ = 'derived_field',
def __init__(self, derived_field, **kwargs):
super().__init__(**kwargs)
self.derived_field = derived_field
You can just put this in a function, and re-use the code in various hierarchies.
def slotted_repr(obj):
slots = [
slot
for klass in type(obj).mro() if hasattr(klass, '__slots__')
for slot in klass.__slots__
]
args = ", ".join(
[f"{slot}={getattr(obj, slot)!r}" for slot in reversed(slots)]
)
return f"{type(obj).__name__}({args})"
Upvotes: 3