0

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!

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
Tim Stewart
  • 5,350
  • 2
  • 30
  • 45

1 Answers1

3

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})"
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • 1
    I can't decide if that's more evil or genius, but I love it. – Kirk Strauser Dec 17 '20 at 18:52
  • @juanpa.arrivillaga I explored the `mro` approach and it did everything I wanted but I held out hope that there was a more pythonic way I didn't know about. I really like the idea of factoring it into a function. Combine that with a function named dict_repr and that would be very helpful. Thanks!! – Tim Stewart Dec 17 '20 at 20:09
  • For posterity, the `hasattr()` check is important because object, the last class in the MRO, does not have `__slots__`. – Tim Stewart Dec 17 '20 at 20:14