Tim Stewart
Tim Stewart

Reputation: 5500

How should I define a __repr__ for a class hierarchy in python?

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:

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

Answers (1)

juanpa.arrivillaga
juanpa.arrivillaga

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

Related Questions