2

After much searching, the only way I have found to solve my particular problem is to use dynamic inheritance. It is easy enough following the guide from here and a few other SO questions; most table is this.

Using a modified version of the contrived example from the first link:

def makeinst(cls, *args, **kwargs):
    class NewClass(cls): pass
    return NewClass(*args, **kwargs)
mylist = makeinst(list,(1,2))

This works as I would hope but it can't be pickled:

pickle.dumps(mylist)
...

AttributeError: Can't pickle local object 'makeinst.<locals>.NewClass'

I understand why this doesn't work but what I want to know is there a way around it? Is there a better way to dynamically subclass something?

(FWIW, dill can't do it either. See dill issue #56)

martineau
  • 119,623
  • 25
  • 170
  • 301
Justin Winokur
  • 91
  • 1
  • 1
  • 7

1 Answers1

1

You can create the class in the global scope of the module, to do that you need to create a class manually with the type(name, bases, class_dict) call

import pickle

def makeinst(name, cls, *args, **kwargs):

    # This will be a method
    def foo(self):
        return f"I am: {self!r}"

    globals()[name] = type(
        name,
        # This must be a tuple
        # (cls) evaluate to cls
        # (cls,) evaluates to a tuple containing cls as its only element
        (cls,),
        # Methods, classmethods, staticmethods and all class-level data
        {
            "foo": foo
        },
    )

    return globals()[name](*args, **kwargs)

my_list = makeinst("MyList", list, [1, 2, 3])
print(my_list) # [1, 2, 3]

data = pickle.dumps(my_list)
my_list_unpickled = pickle.loads(data)
print(my_list_unpickled) # [1, 2, 3]

print(my_list_unpickled.foo()) # I am: [1, 2, 3]

In another program execution you must call makeinst("MyList", list) at least once before unpickling to define the class.

Luiz Ferraz
  • 1,427
  • 8
  • 13
  • That does work for what I asked (Thanks!) but I should have added an additional requirement (sorry I didn't think of it). This solution doesn't work across sessions. So if I save the pickle then reload it without having every called `makeinst("MyList",...)`, it fails. Any way around that? (not hopeful...) – Justin Winokur Dec 14 '19 at 20:29
  • 1
    I don't think that is possible, at least for the current implementation of pickle. To rebuild an instance it calls the `__new__` method of the class and looks for the class by name. You can customize how it discovers the state and what is passed to `__new__` but not how the class is discovered. – Luiz Ferraz Dec 14 '19 at 20:47