How to convert an object back into the code used to create it?

Viewed 74

For example if I have a custom Python object like this;

#!/usr/bin/env python3
import os
base_dir = os.path.abspath(".")
class MyFile(dict):
    def __init__(self, name, size = None, dir = base_dir):
        self.name = name
        self.path = os.path.join(dir, name)
        self.bytes = size

and somewhere in my program, I initialize my object class;

a = MyFile(name = "foo", size = 10)

I want to be able to return the code used to create the object in the first place. For example;

print(a)
# <__main__.MyFile object at 0x102b84470>
# should instead print:
# MyFile(name = "foo", size = 10)

But since my object has some default attribute values, I only want those to show up in the output if they were explicitly included when the object was initialized;

b = MyFile(name = "bar", dir = "/home")
print(b)
# <__main__.MyFile object at 0x102b845c0>
# should instead print:
# MyFile(name = "bar", dir = "/home")

And to be clear, I am not trying to pull this from the source code, because a lot of my objects will be created dynamically, and I want to be able to return the same thing for them as well;

l = [ ("baz", 4), ("buzz", 12) ]
f = [ MyFile(name = n, size = s) for n, s in l ]
print(f)
# [<__main__.MyFile object at 0x1023844a8>, <__main__.MyFile object at 0x102384828>]
# should instead print:
# [ MyFile(name = "baz", size = 4), MyFile(name = "buzz", size = 12) ]

I saw the inspect library (https://docs.python.org/3/library/inspect.html) but it does not seem to have anything that does this. What am I missing? This functionality would be pretty analogous to R's dput function.

3 Answers

At a very basic level you can do this:

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

    def __repr__(self):
        return f'{self.__class__.__name__}({self.a}, {self.b})'


class MyOtherClass(MyClass):
    def method(self):
        pass


c = MyClass(1, 2)
oc = MyOtherClass(3, 4)
print(c, oc)

Result:

MyClass(1, 2) MyOtherClass(3, 4)

This does what you ask, as well as taking subclassing into account to provide the correct class name. But of course things can get complicated for several reasons:

class MyClass:
    def __init__(self, a, b):
        self.a = a + 1
        self.b = b if b < 10 else a
        self.c = 0
        
    def inc_c(self):
        self.c += 1
        
    def __repr__(self):
        return f'{self.__class__.__name__}({self.a - 1}, {self.b})'

The value of c isn't covered by the constructor, so the proposed call would set it to 0. And Although you could compensate for the + 1 for a, the value of b will be more complicated - even more so if you realise someone could have changed the value later.

And then you need to consider that subclasses can override behaviour, etc. So, doing something like this only makes sense in very limited use cases.

As simple as replacing your code snippet with the following:

import os
base_dir = os.path.abspath(".")
class MyFile(object):
    def __init__(self, name, size = None, dir = base_dir):
        self.name = name
        self.path = os.path.join(dir, name)
        self.bytes = size
        self.remember(name,size, dir)
    def remember(self, name,size, dir):
        self.s= '{}(name = \'{}\'{}{})'.format(self.__class__.__name__,name, ", size="+str(size) if size!=None else "", ', dir="'+dir+'"' if dir!=base_dir else "")

        
    def __repr__(self):
        return self.s

a) for a it returns:

MyFile(name = 'foo', size=10)

b) for b it returns:

MyFile(name = 'bar', dir="/home")

c) for f it returns:

[MyFile(name = 'baz', size=4), MyFile(name = 'buzz', size=12)]

Thanks to everyone who commented and answered. Ultimately, I incorporated their ideas and feedback into the following method, which allowed me to preserve the object's native __repr__ while still getting the behaviors I wanted.

#!/usr/bin/env python3
import os

base_dir = os.path.abspath(".")

class MyFile(dict):
    """
    A custom dict class that auto-populates some keys based on simple input args

    compatible with unittest.TestCase.assertDictEqual
    """
    def __init__(self, name, size = None, dir = base_dir):
        """
        standard init methods
        """
        self.name = name
        self.path = os.path.join(dir, name)
        self.bytes = size
        # auto-populate this key
        self['somekey'] = self.path + ' ' + str(self.bytes)
        # more logic for more complex keys goes here...

        # use these later with `init` and `repr`
        self.args = None
        self.kwargs = None

    @classmethod
    def init(cls, *args, **kwargs):
        """
        alternative method to initialize the object while retaining the args passed
        """
        obj = cls(*args, **kwargs)
        obj.args = args
        obj.kwargs = kwargs
        return(obj)

    def repr(self):
        """
        returns a text representation of the object that can be used to
        create a new copy of an identical object, displaying only the
        args that were originally used to create the current object instance
        (do not show args that were not passed e.g. default value args)
        """
        n = 'MyFile('
        if self.args:
            for i, arg in enumerate(self.args):
                n += arg.__repr__()
                if i < len(self.args) - 1 or self.kwargs:
                    n += ', '
        if self.kwargs:
            for i, (k, v) in enumerate(self.kwargs.items()):
                n += str(k) + '=' + v.__repr__()
                if i < len(self.kwargs.items()) - 1:
                    n += ', '
        n += ')'
        return(n)

Usage:

# normal object initialization
obj1 = MyFile('foo', size=10)
print(obj1) # {'somekey': '/Users/me/test/foo 10'}

# initialize with classmethod instead to preserve args
obj2 = MyFile.init("foo", size = 10)
print(obj2) # {'somekey': '/Users/me/test/foo 10'}

# view the text representation
repr = obj2.repr()
print(repr) # MyFile('foo', size=10)

# re-load a copy of the object from the text representation
obj3 = eval(repr)
print(obj3) # {'somekey': '/Users/me/test/foo 10'}

The use case for this being where I need to represent large simple data structures (dicts) in my Python code (integration tests), where the data values are dynamically generated from a smaller set of variables. But when I have many hundreds of such data structures that I need to include in the test case, it becomes infeasible to write the code for e.g. MyFile(...) out hundreds of times. This method allows me to use a script to ingest the data, and then print out compact Python code needed to recreate the data using my custom object class. Which I can then just copy/paste into my test cases.

Related