Reputation: 2274
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
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
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
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