Search code examples
pythonpython-3.xslotsrepr

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:

  • When I pass both 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")
  • I can safely add fields to the 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.
  • The code is minimal. If I have to add more than a couple of lines, the extra code might not be worth it

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!


Solution

  • 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})"