Reputation: 3234
I know this is probably bad design, but I've run into a case where I need to create a subclass Derived
of a class Base
on-the-fly, and make instances of Derived
fail the issubclass(Derived, Base)
or isinstance(derived_obj, Base)
checks (i.e. return False
).
I've tried a number of approaches, but none succeeded:
__class__
in Derived
(https://stackoverflow.com/a/42958013/4909228). This can only be used to make the checks return True
.__instancecheck__
and __subclasscheck__
methods of Base
. This doesn't work because CPython only calls these methods when conventional checks return False
.__class__
attribute during __init__
. This is no longer allowed in Python 3.6+.Derived
subclass object
and assigning all its attributes and methods (including special methods) to that of Base
. This doesn't work because certain methods (e.g. __init__
) cannot be called on an instance that is not a subclass of Base
.Can this possibly be done in Python? The approach could be interpreter specific (code is only run in CPython), and only needs to target Python versions 3.6+.
To illustrate a potential usage of this requirement, consider the following function:
def map_structure(fn, obj):
if isinstance(obj, list):
return [map_structure(fn, x) for x in obj]
if isinstance(obj, dict):
return {k: map_structure(fn, v) for k, v in obj.items()}
# check whether `obj` is some other collection type
...
# `obj` must be a singleton object, apply `fn` on it
return fn(obj)
This method generalizes map
to work on arbitrarily nested structures. However, in some cases we don't want to traverse a certain nested structure, for instance:
# `struct` is user-provided structure, we create a list for each element
struct_list = map_structure(lambda x: [x], struct)
# somehow add stuff into the lists
...
# now we want to know how many elements are in each list, so we want to
# prevent `map_structure` from traversing the inner-most lists
struct_len = map_structure(len, struct_list)
If the said functionality can be implemented, then the above could be changed to:
pseudo_list = create_fake_subclass(list)
struct_list = map_structure(lambda x: pseudo_list([x]), struct)
# ... and the rest of code should work
Upvotes: 2
Views: 588
Reputation: 363596
Overriding the
__instancecheck__
and__subclasscheck__
methods ofBase
. This doesn't work because CPython only calls these methods when conventional checks returnFalse
.
This statement is a misconception. These hooks are to be defined on the metaclass, not on a base class (docs).
>>> class Meta(type):
... def __instancecheck__(self, instance):
... print("instancecheck", self, instance)
... return False
... def __subclasscheck__(self, subclass):
... print("subclasscheck", self, subclass)
... return False
...
>>> class Base(metaclass=Meta):
... pass
...
>>> class Derived(Base):
... pass
...
>>> obj = Derived()
>>> isinstance(obj, Base)
instancecheck <class '__main__.Base'> <__main__.Derived object at 0xcafef00d>
False
>>> issubclass(Derived, Base)
subclasscheck <class '__main__.Base'> <class '__main__.Derived'>
False
Be aware of the CPython performance optimizations which prevent custom instance check hooks from being called in some special cases (see here for details). In particular, you may not strong-arm the return value of isinstance(obj, Derived)
because of a CPython fast path when there was an exact match.
As a final note, I agree with the commenters that it's not sounding like a very promising design. It seems like you should consider using composition over inheritance in this case.
Upvotes: 5
Reputation: 4061
As others have pointed out: use composition.
Make a class that is not recursively mapped:
class NotMapped:
pass
Then use composition and derive your classes from it:
class Derived(NotMapped):
pass
Then add a case at the beginning of your function:
def map_structure(fn, obj):
if isinstance(obj, NotMapped):
return obj
# special-case container types etc.
...
return fn(obj)
Use this together with multiple inheritance as a sort of mixin. Create convenience ctors for container types, so NotMapped([1, 2, 3]) works as expected.
Upvotes: 1