Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly implement both __str__ and __repr__

Tags:

python

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?

like image 436
Jonas Adler Avatar asked Jul 17 '17 20:07

Jonas Adler


2 Answers

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')
like image 57
Aran-Fey Avatar answered Sep 28 '22 08:09

Aran-Fey


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)
like image 26
poke Avatar answered Sep 28 '22 08:09

poke