Search code examples
pythonrepr

variable repr in python based on call depth


I have a (bad?) habit of displaying classes in Python like structures in Matlab, where each attribute is printed along with its value in a nice clean layout. This is done by implementing the __repr__ method in the class.

When working with objects inside of dictionaries or lists, this display style can be a bit distracting. In this case I'd like to do a more basic display.

Here's the envisioned pseudocode:

def __repr__(self):
    if direct_call():
        return do_complicated_printing(self)
    else:
        #something simple that isn't a ton of lines/characters
        return type(self)

In this code direct_call() means that this isn't being called as part of another display call. Perhaps this might entail looking for repr in the stack? How would I implement direct call detection?

So I might have something like:

>>> data
<class my_class> with properties:

        a: 1
   cheese: 2
     test: 'no testing'

But in a list I'd want a display like:

>>> data2 = [data, data, data, data]
>>> data2
[<class 'my_class'>,<class 'my_class',<class 'my_class'>,<class 'my_class'>]

I know it is possible for me to force this type of display by calling some function that does this, but I want my_class to be able to control this behavior, without extra work from the user in asking for it.

In other words, this is not a solution:

>>> print_like_I_want(data2)

Solution

  • This is a strange thing to want to do, and generally a function or method ought to do the same thing whoever is calling it. But in this case, __repr__ is only meant for the programmer's convenience, so convenience seems like a good enough reason to make it work the way you're asking for.

    However, unfortunately what you want isn't actually possible, because for whatever reason, the list.__repr__ method isn't visible on the stack. I tested in Python 3.5.2 and Python 3.8.1:

    >>> class ReprRaises:
    ...     def __repr__(self):
    ...         raise Exception()
    ... 
    >>> r = ReprRaises()
    >>> r
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 3, in __repr__
    Exception
    >>> [r]
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 3, in __repr__
    Exception
    

    As you can see, the stack is the same whether or not the object being repr'd is in a list. (The __repr__ frame on the stack belongs to the ReprRaises class, not list.)

    I also tested using inspect.stack:

    >>> import inspect
    >>> class ReprPrints:
    ...     def __repr__(self):
    ...         print(*inspect.stack(), sep='\n')
    ...         return 'foo'
    >>> r = ReprPrints()
    >>> r
    FrameInfo(frame=<frame object at 0x7fcbe4a38588>, filename='<stdin>', lineno=3, function='__repr__', code_context=None, index=None)
    FrameInfo(frame=<frame object at 0x7fcbe44fb388>, filename='<stdin>', lineno=1, function='<module>', code_context=None, index=None)
    foo
    >>> [r]
    FrameInfo(frame=<frame object at 0x7fcbe4a38588>, filename='<stdin>', lineno=3, function='__repr__', code_context=None, index=None)
    FrameInfo(frame=<frame object at 0x7fcbe44fb388>, filename='<stdin>', lineno=1, function='<module>', code_context=None, index=None)
    [foo]
    

    Again, there's no visible difference in the call stack between the object itself vs. the object in a list; so there's nothing for your __repr__ to check for.


    So, the closest you can get is some kind of print_like_I_want function. This can at least be written in a way that lets each class define its own behaviour:

    def pp(obj):
        try:
            _pp = obj._pp
        except AttributeError:
            print(repr(obj))
        else:
            print(_pp())
    

    The only way I can think of to do it with fewer keypresses is by overloading a unary operator, like the usually-useless unary plus:

    >>> class OverloadUnaryPlus:
    ...     def __repr__(self):
    ...         return 'foo'
    ...     def __pos__(self):
    ...         print('bar')
    ... 
    >>> obj = OverloadUnaryPlus()
    >>> obj
    foo
    >>> +obj
    bar