0

It bugs me that the default __repr__() for a class is so uninformative:

>>> class Opaque(object): pass
... 
>>> Opaque()
<__main__.Opaque object at 0x7f3ac50eba90>

... so I've been thinking about how to improve it. After a little consideration, I came up with this abstract base class which leverages the pickle protocol's __getnewargs__() method:

from abc import abstractmethod

class Repro(object):

    """Abstract base class for objects with informative ``repr()`` behaviour."""

    @abstractmethod
    def __getnewargs__(self):
        raise NotImplementedError

    def __repr__(self):
        signature = ", ".join(repr(arg) for arg in self.__getnewargs__())
        return "%s(%s)" % (self.__class__.__name__, signature)

Here's a trivial example of its usage:

class Transparent(Repro):

    """An example of a ``Repro`` subclass."""

    def __init__(self, *args):
        self.args = args

    def __getnewargs__(self):
        return self.args

... and the resulting repr() behaviour:

>>> Transparent("an absurd signature", [1, 2, 3], str)
Transparent('an absurd signature', [1, 2, 3], <type 'str'>)
>>> 

Now, I can see one reason Python doesn't do this by default straight away - requiring every class to define a __getnewargs__() method would be more burdensome than expecting (but not requiring) that it defines a __repr__() method.

What I'd like to know is: how dangerous and/or fragile is it? Off-hand, I can't think of anything that could go terribly wrong except that if a Repro instance contained itself, you'd get infinite recursion ... but that's solveable, at the cost of making the code above uglier.

What else have I missed?

Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
  • `repr` was around *way* before there existed anything such as `__getnewargs__`. Also you are abusing a method. It was introduced by a library module(not the python language core itself) to do a completely different thing. Also what about keyword arguments? In python3 one can have keyword only arguments for constructor. Also, I do not see any real added benefit from this. Objects that could have a simple string representation *ought* to implement `__repr__` and/or `__str__`. With your method either you still have to reimplement it or you must reimplement `__getnewargs__`. – Bakuriu Nov 18 '12 at 21:09
  • There's no real reason to use `__getnewargs__` per se for this, is there? You could just define a convention whereby an object stores some identifying info in an arbitrary attribute (say `_reprInfo`), and then the `__repr__` just prints `self._reprInfo`. – BrenBarn Nov 18 '12 at 21:14
  • @Bakuriu How am I abusing it, though? A correctly implemented subclass of `Repro` will have a correct-as-per-pickle `__getnewargs__()`, which might even be considered an advantage. I realise this won't work for classes with keyword-arg constructors, but I wouldn't use it with those ... actually, I'd avoid keyword arguments in constructors altogether if I could. – Zero Piraeus Nov 18 '12 at 21:15
  • @BrenBarn yes, originally I was thinking of just using something like a `_reprInfo` property ... but is there any harm in doing it this way? – Zero Piraeus Nov 18 '12 at 21:16
  • @ZeroPiraeus: There's no real "harm", but as @Bakuriu said, it just creates a needless dependence between the `__repr__` and `__getnewargs__`. You could just as well overload any other random method like `__pos__` and declare that that method should now be used to return the `__repr__` data, but why? It doesn't have any advantage, and it just will cause a headache if you later want to use both `__repr__` and the overloaded method for their own separate purposes. – BrenBarn Nov 18 '12 at 21:19
  • @ZeroPiraeus If you use only in your classes it may be ok. I'm saying about using it as default by the language itself. As I said, if you want to give a certain representation just implement `repr`. If you do not need it then you do not need it and you are addressing a problem that does not exist. – Bakuriu Nov 18 '12 at 21:20
  • @BrenBarn are you saying that a correct for Repro `__getnewargs__()` is an incorrect for pickle `__getnewargs__()`, then? If so, I've missed something. Just to be clear: I'm not proposing this as default Python behaviour (I thought I'd already made that clear in the question, to be honest) - just a potential convenience for my own code. – Zero Piraeus Nov 18 '12 at 21:24
  • @ZeroPiraeus: It may be, but there's no necessary reason it has to be. I'm just saying that there is no reason to use `__getnewargs__` for this because conceptually it has no connection to what you're trying to do. – BrenBarn Nov 18 '12 at 21:27
  • @BrenBarn but it *does* have a connection. `__getnewargs__()` is meant to return the arguments required to reconstruct the object, and `repr()` is meant to return a representation that, ideally, can be used to reconstruct the object. I didn't just pick that method out of the sky ;-) – Zero Piraeus Nov 18 '12 at 21:33

2 Answers2

4

If you're into this sort of thing, why not have the arguments automatically picked up from __init__ by using a decorator? Then you don't need to burden the user with manually handling them, and you can transparently handle normal method signatures with multiple arguments. Here's a quick version I came up with:

def deco(f):
    def newFunc(self, *args, **kwargs):
        self._args = args
        self._kwargs = kwargs
        f(self, *args, **kwargs)
    return newFunc
class AutoRepr(object):
    def __repr__(self):
        args = ', '.join(repr(arg) for arg in self._args)
        kwargs = ', '.join('{0}={1}'.format(k, repr(v)) for k, v in self._kwargs.iteritems())
        allArgs = ', '.join([args, kwargs]).strip(', ')
        return '{0}({1})'.format(self.__class__.__name__, allArgs)

Now you can define subclasses of AutoRepr normally, with normal __init__ signatures:

class Thingy(AutoRepr):
    @deco
    def __init__(self, foo, bar=88):
        self.foo = foo
        self.bar = bar

And the __repr__ automatically works:

>>> Thingy(1, 2)
Thingy(1, 2)
>>> Thingy(10)
Thingy(10)
>>> Thingy(1, bar=2)
Thingy(1, bar=2)
>>> Thingy(bar=1, foo=2)
Thingy(foo=2, bar=1)
>>> Thingy([1, 2, 3], "Some junk")
Thingy([1, 2, 3], 'Some junk')

Putting @deco on your __init__ is much easier than writing a whole __getnewargs__. And if you don't even want to have to do that, you could write a metaclass that automatically decorates the __init__ method in this way.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • That *is* rather elegant, yes ... but it would mean you'd have to update `_args` and `_kwargs` whenever you modified the object. For me, the advantage of using `__getnewargs__()` is that in order to be correct for `pickle`, it has to be correct for `repr` and vice versa, which I think might be less fragile (or at least more likely to break early and conspicuously). – Zero Piraeus Nov 18 '12 at 21:49
  • @ZeroPiraeus: A metaclass could check to see if there was a `__getnewargs__` defined and use that to generate a `__repr__()` and otherwise assume that the decorator had been used on the `__init__()`. – martineau Nov 18 '12 at 23:49
  • @martineau hmmm ... combining that with BrenBarn's final suggestion above, we'd have a metaclass that either decorates one special method or creates another, based on the existence or otherwise of a third. That's a lot of magic ... – Zero Piraeus Nov 19 '12 at 00:23
  • @ZeroPiraeus: If you really want to keep track of the object's state on the fly (so that changes to attributes are reflected in `__repr__` and `__getnewargs__`), just create your own method that manages this, keeping track of state changes, and then have both `__repr__` and `__getnewargs__` call out to that method to get the scoop on what the current state is. – BrenBarn Nov 19 '12 at 00:28
  • @BrenBarn interesting idea ... I'll have to go away and think about that. Thanks :-) – Zero Piraeus Nov 19 '12 at 00:34
2

One problem with this whole idea is that there can be some kinds of objects who's state is not fully dependent on the arguments given to its constructor. For a trivial case, consider a class with random state:

import random

def A(object):
    def __init__(self):
        self.state = random.random()

There's no way for this class to correctly implement __getnewargs__, and so your implantation of __repr__ is also impossible. It may be that a class like the one above is not well designed. But pickle can handle it with no problems (I assume using the __reduce__ method inherited from object, but my pickle-fu is not enough to say so with certainty).

This is why it is nice that __repr__ can be coded to do whatever you want. If you want the internal state to be visible, you can make your class's __repr__ do that. If the object should be opaque, you can do that too. For the class above, I'd probably implement __repr__ like this:

def __repr__(self):
    return "<A object with state=%f>" % self.state
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • That's an excellent counterexample, and one I hadn't considered - thanks! I'll wait before accepting to see if any others crop up ... you never know ... – Zero Piraeus Nov 18 '12 at 22:52
  • Seems to me like this class could implment `__getnewargs` if it also implemented a `__new__` method that accepted a state argument. – martineau Dec 04 '12 at 11:31