Zecong Hu
Zecong Hu

Reputation: 3234

How to create a Python class that is a subclass of another class, but fails issubclass and/or isinstance tests?

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:

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

Answers (2)

wim
wim

Reputation: 363596

Overriding the __instancecheck__ and __subclasscheck__ methods of Base. This doesn't work because CPython only calls these methods when conventional checks return False.

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

FRob
FRob

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

Related Questions