Lav
Lav

Reputation: 2274

Create/imitate mutable subclass of immutable built-in type

The problem:

I have implemented a class with rather complex internal behavior which pretends to be an int type for all intents and purposes. Then, as a cherry on top, I really wanted my class to successfully pass isinstance() and issubclass() checks for int. I failed so far.

Here's a small demo class that I'm using to test the concept. I have tried inheriting it from both object and int, and while inheriting it from int makes it pass the checks, it also breaks some of it's behavior:

#class DemoClass(int):
class DemoClass(object):
    _value = 0
    def __init__(self, value = 0):
        print 'init() called'
        self._value = value
    def __int__(self):
        print 'int() called'
        return self._value + 2
    def __index__(self):
        print 'index() called'
        return self._value + 2
    def __str__(self):
        print 'str() called'
        return str(self._value + 2)
    def __repr__(self):
        print 'repr() called'
        return '%s(%d)' % (type(self).__name__, self._value)
    # overrides for other magic methods skipped as irrelevant

a = DemoClass(3)

print a         # uses __str__() in both cases
print int(a)    # uses __int__() in both cases
print '%d' % a  # __int__() is only called when inheriting from object

rng = range(10)
print rng[a]    # __index__() is only called when inheriting from object

print isinstance(a, int)
print issubclass(DemoClass, int)

Essentially, inheriting from an immutable class results in an immutable class, and Python will often use base class raw value instead of my carefully-designed magic methods. Not good.

I have looked at abstract base classes, but they seem to be doing something entirely opposite: instead of making my class look like a subclass of an built-in type, they make a class pretend to be a superclass to one.

Using __new__(cls, ...) doesn't seem like a solution either. It's good if all you want is modify object starting value before actually creating it, but I want to evade the immutability curse. Attempt to use object.__new__() did not bear fruit either, as Python simply complained that it's not safe to use object.__new__ to create an int object.

Attempt to inherit my class from (int, dict) and use dict.__new__() was not very successful either as Python apparenty doesn't allow to combine them in a single class.

I suspect the solution might be found with metaclasses, but so far haven't been successful with them either, mostly because my brains simply aren't bent enough to comprehend them properly. I'm still trying but it doesn't look like I'll be getting results soon.

So, the question: is it possible at all to inherit or imitate inheritance from immutable type even though my class is very much mutable? Class inheritance structure doesn't really matter for me for as long as a solution is found (assuming it exists at all).

Upvotes: 3

Views: 650

Answers (3)

Lav
Lav

Reputation: 2274

So far, no alternative solutions have been suggested, so here's the solution that I'm using in the end (loosely based on Serge Ballesta's answer):

def forge_inheritances(disguise_heir = {}, disguise_type = {}, disguise_tree = {},
                       isinstance = None, issubclass = None, type = None):
    """
    Monkey patch isinstance(), issubclass() and type() built-in functions to create fake inheritances.

    :param disguise_heir: dict of desired subclass:superclass pairs; type(subclass()) will return subclass
    :param disguise_type: dict of desired subclass:superclass pairs, type(subclass()) will return superclass
    :param disguise_tree: dict of desired subclass:superclass pairs, type(subclass()) will return superclass for subclass and all it's heirs
    :param isinstance: optional callable parameter, if provided it will be used instead of __builtins__.isinstance as Python real isinstance() function.
    :param issubclass: optional callable parameter, if provided it will be used instead of __builtins__.issubclass as Python real issubclass() function.
    :param type: optional callable parameter, if provided it will be used instead of __builtins__.type as Python real type() function.
    """

    if not(disguise_heir or disguise_type or disguise_tree): return

    import __builtin__
    from itertools import chain

    python_isinstance = __builtin__.isinstance if isinstance is None else isinstance
    python_issubclass = __builtin__.issubclass if issubclass is None else issubclass
    python_type       = __builtin__.type if type is None else type

    def disguised_isinstance(obj, cls, honest = False):
        if cls == disguised_type: cls = python_type
        if honest:
            if python_isinstance.__name__ == 'disguised_isinstance':
                return python_isinstance(obj, cls, True)
            return python_isinstance(obj, cls)
        if python_type(cls) == tuple:
            return any(map(lambda subcls: disguised_isinstance(obj, subcls), cls))
        for subclass, superclass in chain(disguise_heir.iteritems(),
                                          disguise_type.iteritems(),
                                          disguise_tree.iteritems()):
            if python_isinstance(obj, subclass) and python_issubclass(superclass, cls):
                return True
        return python_isinstance(obj, cls)
    __builtin__.isinstance = disguised_isinstance

    def disguised_issubclass(qcls, cls, honest = False):
        if cls == disguised_type: cls = python_type
        if honest:
            if python_issubclass.__name__ == 'disguised_issubclass':
                return python_issubclass(qcls, cls, True)
            return python_issubclass(qcls, cls)
        if python_type(cls) == tuple:
            return any(map(lambda subcls: disguised_issubclass(qcls, subcls), cls))
        for subclass, superclass in chain(disguise_heir.iteritems(),
                                          disguise_type.iteritems(),
                                          disguise_tree.iteritems()):
            if python_issubclass(qcls, subclass) and python_issubclass(superclass, cls):
                return True
        return python_issubclass(qcls, cls)
    __builtin__.issubclass = disguised_issubclass

    if not(disguise_type or disguise_tree): return # No need to patch type() if these are empty

    def disguised_type(obj, honest = False, extra = None):
        if (extra is not None):
            # this is a call to create a type instance, we must not touch it
            return python_type(obj, honest, extra)
        if honest:
            if python_type.__name__ == 'disguised_type':
                return python_type(obj, True)
            return python_type(obj)
        for subclass, superclass in disguise_type.iteritems():
            if obj == subclass:
                return superclass
        for subclass, superclass in disguise_tree.iteritems():
            if python_isinstance(obj, subclass):
                return superclass
        return python_type(obj)
    __builtin__.type       = disguised_type

if __name__ == '__main__':
    class A(object): pass
    class B(object): pass
    class C(object): pass

    forge_inheritances(disguise_type = { C: B, B: A })

    print issubclass(B, A) # prints True
    print issubclass(C, B) # prints True
    print issubclass(C, A) # prints False - cannot link two fake inheritances without stacking

It is possible to ignore the faked inheritance by providing optional honest parameter to isinstance(), issubclass() and type() calls.

Usage examples.

Make class B a fake heir of class A:

class A(object): pass
class B(object): pass
forge_inheritances(disguise_heir = { B: A })
b = B()
print isinstance(b, A) # prints True
print isinstance(b, A, honest = True) # prints False

Make class B pretend to be class A:

class A(object): pass
class B(object): pass
forge_inheritances(disguise_type = { B: A})
b = B()
print type(b) # prints "<class '__main__.A'>"
print type(b, honest = True) # prints "<class '__main__.B'>"

Make class B and all it's heirs pretend to be class A:

class A(object): pass
class B(object): pass
class D(B): pass
forge_inheritances(disguise_tree = { B: A})
d = D()
print type(d) # prints "<class '__main__.A'>"

Multiple layers of fake inheritances can be achieved by stacking calls to forge_inheritances():

class A(object): pass
class B(object): pass
class C(object): pass
forge_inheritance(disguise_heir = { B: A})
forge_inheritance(disguise_heir = { C: B})
c = C()
print isinstance(c, A) # prints True

Obviously, this hack will not affect super() calls and attribute/method inheritance in any way, the primary intent here is just to cheat isinstance() and type(inst) == class checks in a situation when you have no way to fix them directly.

Upvotes: 0

GingerPlusPlus
GingerPlusPlus

Reputation: 5606

So, if I understand correctly, you have:

def i_want_int(int_):
    # can't read the code; it uses isinstance(int_, int)

And you want call i_want_int(DemoClass()), where DemoClass is convertible to int via __int__ method.

If you want to subclass int, instances' values are determined at creation time.

If you don't want to write conversion to int everywhere (like i_want_int(int(DemoClass()))), the simplest approach I can think about is defining wrapper for i_want_int, doing the conversion:

def i_want_something_intlike(intlike):
    return i_want_int(int(intlike))

Upvotes: 0

Serge Ballesta
Serge Ballesta

Reputation: 148880

The problem here is not immutability, but simply inheritance. If DemoClass is a subclass of int, a true int is constructed for each object of type DemoClass and will be used directly without calling __int__ wherever a int could be used, just try a + 2.

I would rather try to simply cheat isinstance here. I would just make DemoClass subclass of object and hide the built-in isinstance behind a custom function:

class DemoClass(object):
    ...

def isinstance(obj, cls):
    if __builtins__.isinstance(obj, DemoClass) and issubclass(int, cls):
        return True
    else:
        return __builtins__.isinstance(obj, cls)

I can then do:

>>> a = DemoClass(3)
init() called
>>> isinstance("abc", str)
True
>>> isinstance(a, DemoClass)
True
>>> isinstance(a, int)
True
>>> issubclass(DemoClass, int)
False

Upvotes: 4

Related Questions