2

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:

  • Creating a property named __class__ in Derived (https://stackoverflow.com/a/42958013/4909228). This can only be used to make the checks return True.
  • Overriding the __instancecheck__ and __subclasscheck__ methods of Base. This doesn't work because CPython only calls these methods when conventional checks return False.
  • Assigning the __class__ attribute during __init__. This is no longer allowed in Python 3.6+.
  • Making 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
Zecong Hu
  • 2,584
  • 18
  • 33
  • 5
    Yes, that's terrible design. You probably should use composition in some way. – chepner Jun 19 '19 at 00:29
  • 1
    What you're asking for is implementation inheritance without the interface inheritance (or at least, the closest approximation Python has to interface inheritance). And that's a perfect use case for composition. If your object shouldn't be iterated like a `list`, then it isn't a `list`. Plain and simple. To make it a list violates duck typing, Liskov substitution, and probably other principles. – Silvio Mayolo Jun 19 '19 at 00:33
  • Have you tried adding a metaclass to the base implementing `__instancecheck__`? – Mark Jun 19 '19 at 00:35

2 Answers2

5

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.

wim
  • 338,267
  • 99
  • 616
  • 750
  • Thanks for the great answer! One more question: is it possible to dynamically add a metaclass to an existing class (including built-in classes)? – Zecong Hu Jun 19 '19 at 14:33
  • 1
    That's not possible (and it doesn't make a lot of sense to modify the metaclass if the class is already created, since the main responsibility of the metaclass is customizing class creation). For user-defined types, you can possibly dynamically monkeypatch the instance check and subclass check *methods* themselves, but you certainly can not do this on built-in types. – wim Jun 19 '19 at 14:57
1

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.

FRob
  • 3,883
  • 2
  • 27
  • 40