6

In several of my classes, I want to implement both __str__ and __repr__ and usually end up with code like this:

class MyClass(object):
    def __init__(self, a):
        self.a = a

    def __str__(self):
        return 'MyClass({})'.format(self.a)

    def __repr__(self):
        return 'MyClass({!r})'.format(self.a)

Which does what I'd expect:

>>> myobject = MyClass(np.array([1, 2]))
>>> str(myobject)
'MyClass([1 2])'
>>> repr(myobject)
'MyClass(array([1, 2]))'

However the code violates DRY and as the number of arguments starts to grow maintaining this becomes cumbersome and I've often found that either of __str__ or __repr__ has come "out of sync" with the other.

Is there a better way to simultaneously implement both __str__ and __repr__ without duplication?

Jonas Adler
  • 10,365
  • 5
  • 46
  • 73

5 Answers5

5

Since your __str__ and __repr__ follow the same pattern, you could write a function to create the object's string representation for you. It would take an object, a list of attributes and str or repr as arguments:

def stringify(obj, attrs, strfunc):
    values = []
    # get each attribute's value and convert it to a string
    for attr in attrs:
        value = getattr(obj, attr)
        values.append(strfunc(value))

    # get the class name
    clsname = type(obj).__name__

    # put everything together
    args = ', '.join(values)
    return '{}({})'.format(clsname, args)

print( stringify(MyClass('foo'), ['a'], repr) )
# output: MyClass('foo')

I would recommend putting this function in a class which you then inherit from:

class Printable:
    def __str__(self):
        return self.__stringify(str)

    def __repr__(self):
        return self.__stringify(repr)

    def __stringify(self, strfunc):
        values = []
        for attr in self._attributes:
            value = getattr(self, attr)
            values.append(strfunc(value))

        clsname = type(self).__name__
        args = ', '.join(values)
        return '{}({})'.format(clsname, args)

class MyClass(Printable):
    _attributes = ['a']

    def __init__(self, a):
        self.a = a

And you can even get it done completely automatically by grabbing the attributes directly from the __init__ function's signature:

import inspect

class AutoPrintable:
    def __str__(self):
        return self.__stringify(str)

    def __repr__(self):
        return self.__stringify(repr)

    def __stringify(self, strfunc):
        sig= inspect.signature(self.__init__)
        values= []
        for attr in sig.parameters:
            value= getattr(self, attr)
            values.append(strfunc(value))

        clsname= type(self).__name__
        args= ', '.join(values)
        return '{}({})'.format(clsname, args)

class MyClass(AutoPrintable):
    def __init__(self, a, b):
        self.a = a
        self.b = b

print( str(MyClass('foo', 'bar')) ) # output: MyClass(foo, bar)
print( repr(MyClass('foo', 'bar')) ) # output: MyClass('foo', 'bar')
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
4

Neither the official Python documentation nor the Index of Python Enhancement Proposal seem to specify clear guidelines on how to override these methods, except for the 3.3 Special method names which, among other things, says of __repr__():

If at all possible, this should look like a valid Python expression that could be used to recreate an object with the same value [...] This is typically used for debugging, so it is important that the representation is information-rich and unambiguous.

I like to take inspiration from how __repr__() is implemented in some of the standard library modules, take e.g. socket.socket:

$ python3
>>> from socket import socket
>>> socket()
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>

So the pattern here is <self.__class__.__module__ + "." + self.__class__.__name__ attribute1=value1, ..., attributeN=valueN>.

While __repr__() is preferred for debug/testing purposes, the scope of __str__() is much more informal and I'd deduce that even looser rules apply. Note that if __repr__() is overridden but __str__() is not, __repr__() calls __str__().

Here again, if I have to pick some rules I prefer to have __str__() resemble __repr__(), but modifying:

  • The number of items displayed. I don't need to be verbose as __repr__ mandates.
  • The type of values displayed. I include the most "important" ones, and even values that do not reflect the arguments initially passed to __init__().

Another few examples come from a PDF library I've been working on. There are two PdfFileReader and PdfFileWriter classes, and their __repr__() and __str__() methods have the following output:

r = PdfFileReader("samplecode/pdfsamples/jpeg.pdf")
w = PdfFileWriter()

print(r)
print(str(r))
print(repr(r))

print(w)
print(str(w))
print(repr(w))

$ python3 repr.py
<pypdf.pdf.PdfFileReader _filepath=samplecode/pdfsamples/jpeg.pdf, stream=<_io.BytesIO object at 0x7eff60f07e60>, strict=True, debug=False>
<pypdf.pdf.PdfFileReader _filepath=samplecode/pdfsamples/jpeg.pdf, stream=<_io.BytesIO object at 0x7eff60f07e60>, strict=True, debug=False>
<pypdf.pdf.PdfFileReader _filepath=samplecode/pdfsamples/jpeg.pdf, stream=<_io.BytesIO object at 0x7eff60f07e60>, strict=True, debug=False>
<pypdf.pdf.PdfFileWriter _header=%PDF-1.3, debug=False>
<pypdf.pdf.PdfFileWriter _header=%PDF-1.3, debug=False>
<pypdf.pdf.PdfFileWriter _header=%PDF-1.3, debug=False>

See also 2. Built-in Functions for repr():

[...] For many types, this function makes an attempt to return a string that would yield an object with the same value when passed to eval(), otherwise the representation is a string enclosed in angle brackets that contains the name of the type of the object together with additional information often including the name and address of the object. [...]

Acsor
  • 1,011
  • 2
  • 13
  • 26
3

There are no rules nor clear guidelines for implementing __str__ and __repr__—at least none that are consistently followed anywhere (not even in the stdlib). So there wouldn’t be a way to get the “standard behavior” automatically, simply because there isn’t a standard behavior. It’s up to you, so if you set up guidelines for yourself, maybe you can also come up with a utility to make it easier for you to follow them.

In your case, you could for example create a base class which provides the __str__ and __repr__ implementations:

class AutoStRepr(object):
    _args = []
    def __repr__(self):
        return '{}({})'.format(type(self).__name__,
            ', '.join(repr(getattr(self, a)) for a in self._args))
    def __str__(self):
        return '{}({})'.format(type(self).__name__,
            ', '.join(str(getattr(self, a)) for a in self._args))

You could then use that on a number of different types:

class MyClass(AutoStRepr):
    _args = ['a']
    def __init__(self, a):
        self.a = a

class MyOtherClass(AutoStRepr):
    _args = ['a', 'bc']
    def __init__(self, a, b, c):
        self.a = a
        self.bc = b * c
>>> MyClass('foo')
MyClass('foo')
>>> MyOtherClass('foo', 2, 5)
MyOtherClass('foo', 10)
poke
  • 369,085
  • 72
  • 557
  • 602
  • Not a bad answer, but in a way this violates the DRY principle just as much as the original solution - you'd probably want to move those two massive `.format`s into a helper function. – Aran-Fey Jul 17 '17 at 21:27
  • 1
    @Rawing Those “massive joins”? How are they massive? It’s just a join over a simple generator expression. And no, that’s not a violation of DRY; any solution to avoid repeating that would just add a crazy amount of complexity that has no actual benefit. DRY is about avoid having the same logic over and over. This here is *two times* and it’s actually a different logic which might end up diverging even more. – poke Jul 17 '17 at 21:31
2

No need for duplication, just don't implement __str__.

That way, the object will behave like __str__ = __repr__.

I think you should also read this answer.

bergerg
  • 985
  • 9
  • 23
  • I am well aware of the possibility to not duplicate, but for "large" objects, the difference, here just `array(...)`, can be quite large and having both str and repr can be useful. – Jonas Adler Jul 17 '17 at 21:10
  • Then follow the `__repr__` is unambiguous `__str__` is readable guideline. The implementation is totaly up to your definition of unambiguous and readable. – bergerg Jul 17 '17 at 21:15
  • I think this is an bad idea, because: `__str__` has the purpose to return an **usable and readable representation** by `str()`. But `__repr__` has the purpose to return an string for **reproducing** the instance by `eval(repr())`. – Sukombu Jun 28 '22 at 17:11
0

How about this example:

#!/usr/bin/env python3

class Quaternion:
    _x: float = 0.0
    _y: float = 0.0
    _z: float = 0.0
    _w: float = 1.0

    @property
    def x(self) -> float:
        return self._x

    @property
    def y(self) -> float:
        return self._y

    @property
    def z(self) -> float:
        return self._z

    @property
    def w(self) -> float:
        return self._w

    def __init__(self, x: float, y: float, z: float, w: float) -> None:
        self._x = float(x)
        self._y = float(y)
        self._z = float(z)
        self._w = float(w)

    def __str__(self) -> str:
        return ", ".join(
            (
                str(self._x),
                str(self._y),
                str(self._z),
                str(self._w)
            )
        )

    def __repr__(self) -> str:
        cls = self.__class__
        module = cls.__module__
        return f"{module + '.' if module != '__main__' else ''}{cls.__qualname__}({str(self)})"

Pros

  • str() returns the pure values: 0.0, 0.0, 0.0, 1.0
  • repr() returns something like: Quaternion(0.0, 0.0, 0.0, 1.0) or mymodule.Quaternion(0.0, 0.0, 0.0, 1.0) (if it's in an imported submodule). So you are able to reproduce it easily by eval(repr())

Cons

Don't know any, yet.

Sukombu
  • 157
  • 3
  • 12