4

I'm using a package that gives me an object filled with a bunch of data that I don't want to bother to manually serialize and use to initialize another object. What I want to do is attach a bunch of extra methods onto the object for my own purposes.

Ideally I'd like to magically subclass an instance, but that doesn't seem possible. Monkey-patching would likely 'work', but the internet says it's not great form, and because other parts of my code might actually use the native class elsewhere, seems perilous.

I tried creating a wrapper object, but many (all?) of the magic methods (e.g. __iter__) skip the __getattribute__ call, so it's incomplete. Slapping a bunch of pass-through function definitions (e.g. def __iter__(self): return iter(object.__getattribute__(self, '_digraph'))) seems clunky (and I'll probably forget one).

class ColliderGraph(object):
    def __init__(self, digraph):
        self._digraph = digraph

    def __getattribute__(self, name):
        cls_attrs = ['_digraph', 'whereis', 'copy'] # more to follow.
        if name not in cls_attrs:
            return object.__getattribute__(
                    object.__getattribute__(self, '_digraph'), name)
        else:
            return object.__getattribute__(self, name)
        #return getattr(self._digraph, name)

    def whereis(self, node):
        """find a node inside other nodes of the digraph"""

    def copy(self):
        return ColliderGraph(self._digraph.copy())

Somewhere else in a more limited fashion I started to patch the instance with a single odd function like so:

def _whereis_addon(digraph, node):
    """For patching onto specially-modifed digraph objects."""

# then elsewhere...
digraph.whereis = types.MethodType(_whereis_addon, digraph)

But if .copy() is called then it loses its upgrade (I guess I could patch that too...), and adding a bunch of methods this way also seems ugly, but perhaps doable.

Is there a better way out?

Community
  • 1
  • 1
Nick T
  • 25,754
  • 12
  • 83
  • 121
  • 1
    Any reason not to use the wrapping approach, along with the [metaclass-based solution](http://stackoverflow.com/a/9059858/2073595) to dealing with magic methods that kindall provided? – dano Aug 20 '14 at 00:07

1 Answers1

1

First, I think the sanest option would be to patch the digraph instance to add the methods you need, and include patching __copy__ in that, or even stick with your wrapper, and use a metaclass to add proxies for magic methods, as suggested in this answer to the question you linked to.

That said, I was recently toying around with the idea of "magically" subclassing an instance, and figured I'd share my findings with you, since you're toying with the very same thing. Here's the code I came up with:

def retype_instance(recvinst, sendtype, metaklass=type):
    """ Turn recvinst into an instance of sendtype.

    Given an instance (recvinst) of some class, turn that instance 
    into an instance of class `sendtype`, which inherits from 
    type(recvinst). The output instance will still retain all
    the instance methods and attributes it started with, however.

    For example:

    Input:
    type(recvinst) == Connection
    sendtype == AioConnection
    metaklass == CoroBuilder (metaclass used for creating AioConnection)

    Output:
    recvinst.__class__ == AioConnection
    recvinst.__bases__ == bases_of_AioConnection +
                          Connection + bases_of_Connection

    """
    # Bases of our new instance's class should be all the current
    # bases, all of sendtype's bases, and the current type of the
    # instance. The set->tuple conversion is done to remove duplicates
    # (this is required for Python 3.x).
    bases = tuple(set((type(recvinst),) + type(recvinst).__bases__ +
                  sendtype.__bases__))

    # We change __class__ on the instance to a new type,
    # which should match sendtype in every where, except it adds
    # the bases of recvinst (and type(recvinst)) to its bases.
    recvinst.__class__ = metaklass(sendtype.__name__, bases, {})

    # This doesn't work because of http://bugs.python.org/issue672115
    #sendtype.__bases__ = bases
    #recv_inst.__class__ = sendtype

    # Now copy the dict of sendtype to the new type.
    dct = sendtype.__dict__
    for objname in dct:
        if not objname.startswith('__'):
            setattr(type(recvinst), objname, dct[objname])
    return recvinst

The idea is to redefine the __class__ of the instance, changing it to be a new class of our choosing, and add the original value of __class__ to inst.__bases__ (along with the __bases__ of the new type). Additionally, we copy the __dict__ of the new type into the instance. This sounds fairly crazy and probably is, but in the little bit of testing I did with it, it seemed to (mostly) actually work:

class MagicThread(object):
    def magic_method(self):
        print("This method is magic")


t = Thread()
m = retype_instance(t, MagicThread)
print m.__class__
print type(m)
print type(m).__mro__
print isinstance(m, Thread)
print dir(m)
m.magic_method()
print t.is_alive()
print t.name
print isinstance(m, MagicThread)

Output:

<class '__main__.MagicThread'>
<class '__main__.MagicThread'>
(<class '__main__.MagicThread'>, <class 'threading.Thread'>, <class 'threading._Verbose'>, <type 'object'>)
True
['_Thread__args', '_Thread__block', '_Thread__bootstrap', '_Thread__bootstrap_inner', '_Thread__daemonic', '_Thread__delete', '_Thread__exc_clear', '_Thread__exc_info', '_Thread__ident', '_Thread__initialized', '_Thread__kwargs', '_Thread__name', '_Thread__started', '_Thread__stderr', '_Thread__stop', '_Thread__stopped', '_Thread__target', '_Verbose__verbose', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_block', '_note', '_reset_internal_locks', '_set_daemon', '_set_ident', 'daemon', 'getName', 'ident', 'isAlive', 'isDaemon', 'is_alive', 'join', 'magic_method', 'name', 'run', 'setDaemon', 'setName', 'start']
This method is magic
False
Thread-1
False

All of that output is exactly as we'd like, except for the last line - isinstance(m, MagicThread) is False. This is because we didn't actually assign __class__ to the MagicMethod class we defined. Instead, we created a separate class with the same name and all the same methods/attributes. Ideally this could be addressed by actually redefining the __bases__ of MagicThread inside retype_instance, but Python won't allow this:

TypeError: __bases__ assignment: 'Thread' deallocator differs from 'object'

This appears to be a bug in Python dating all the way back to 2003. It hasn't been fixed yet, probably because dynamically redefining __bases__ on an instance is a strange and probably bad idea!

Now, if you don't care about being able to use isinstance(obj, ColliderGraph), the above might work for you. Or it might fail in strange, unexpected ways. I really wouldn't recommend using this in any production code, but I figured I'd share it.

Community
  • 1
  • 1
dano
  • 91,354
  • 19
  • 222
  • 219