Can the default implementation of __repr__ be improved based on the __init__ function?

Viewed 753

As per my understanding __repr__ is used to represent a developer/interpreter friendly representation of the object and should possibly be a valid python code which when passed to eval() recreates an identical object.

From Python Docs:

object.repr(self)

Called by the repr() built-in function and by string conversions (reverse quotes) to compute the “official” string representation of an object. If at all possible, this should look like a valid Python expression that could be used to recreate an object with the same value (given an appropriate environment). If this is not possible, a string of the form <...some useful description...> should be returned. The return value must be a string object. If a class defines repr() but not str(), then repr() is also used when an “informal” string representation of instances of that class is required.

Link: https://docs.python.org/2/reference/datamodel.html#object.repr

E.g:

class tie(object):
    def __init__(self, color):
        self.color = color
t = tie('green')
repr(t) # prints <tie object at 0x10fdc4c10>

# can the default implementation be improved to tie(color='green')
# based on the parameters passed in the __init__ function

What challenges would be there to change that implementation apart from backward compatibility/ existing behavior?

2 Answers

Having default repr based on how the object was created would bring inefficiencies and confusion

Size

the __init__ arguments would have to be stored via copy somewhere in the object - making the object bloated

not all object simply copy those values into themselves

for example:

class GreedyMan:
    def __init__(self, coins):
        self.most_valuable_coin = max(coins)

you would have to save the whole coins collection here

classes are mutable

a Color class that is initialized with 0xff00ff can change over its lifespan into another color

class Color:
    def __init__(self, color):
        self.color = color

    def dilute(self, factor):
        self.color = self.color * factor

the dilute can change the state of the class so you would no longer have a color of 0xff00ff but some other one and if something threw an exception like "I dont accept non red colors - provided Color('red')" the programmer would have some debugging to do until he notices someone used the dilute to get some wierd shade..

so why not print the whole state - all of the class properties

result could be huge/infinite

graphs can have nodes that contain other nodes and even cycles

class ChainPart:
    def __init__(self, parent):
        self.parent = parent
        self.children = []

    def add_child(self, child):
        self.children.append(child)

a=ChainPart(None)
b=ChainPart(a)
b.add_child(a)

the chain part b when printed out with all of its contents recursively would have to print a part which would print part b ... etc ad infinum

so the most obvious way to solve these issues is to leave repr simple and allow the programmer to change it with a custom __repr__ method in the object class.

That's essentially what pickle is trying to do. The idea is that since an object in memory is a graph, if you revisit that graph you can reconstruct the object.

Pickle generates a series of instructions to reconstruct data, but, in principle, it could generate Python code directly.

While I'm going to consider all the problems with it, Pickling is used very heavily for distributed applications where systems need to transmit data from one process to another.

And __repr__ methods are very handy in debugging when you want to be able to reconstruct an object by copying and pasting into a prompt.

Let's look at where pickling doesn't work:

  1. Some objects actually represent objects! A network socket, for instance, identifies the kernel resources managing the actual network connection. Even if you could recreate those, there wouldn't be another machine listening.

  2. Some objects have real world consequences. An object might represent a password or other secret that should not leave the process's memory e.g. by being printed to logs.

  3. To determine which objects to construct first you must perform a topological sort, and while fast algorithms exist, they aren't free. Especially, things can get hairy if another thread modifies the graph while this is happening.

  4. If you save this data and try to reload it on another version, it will fail. If you design new versions to accept the old data, development slows to a crawl as your code becomes more contorted to deal with backwards compatibility.

  5. It is very tightly coupled to your objects and Python's object model, so you wind up reimplementing it.

  6. It has access to anything in Python, so if someone passes in shutil.rmtree('/') the machine will faithfully execute it.

Most of these aren't show stoppers, but to be fixed require tradeoffs. The main reason why the built-in __repr__ does so little is to ensure that simple things stay simple.

And modules like attrs, which inspired the dataclasses module in python 3.7, provide a canned __repr__ that largely does what you're asking for and answers most of the use cases that I think you're imagining.

I'm working on a language that adresses some of these issues, and I summarized some of the different existing approaches to dealing with the issues I mention above.

Related