Dynamic sub-classing in Python

308 views Asked by At

I have a number of atomic classes (Components/Mixins, not really sure what to call them) in a library I'm developing, which are meant to be subclassed by applications. This atomicity was created so that applications can only use the features that they need, and combine the components through multiple inheritance.

However, sometimes this atomicity cannot be ensured because some component may depend on another one. For example, imagine I have a component that gives a graphical representation to an object, and another component which uses this graphical representation to perform some collision checking. The first is purely atomic, however the latter requires that the current object already subclassed this graphical representation component, so that its methods are available to it. This is a problem, because we have to somehow tell the users of this library, that in order to use a certain Component, they also have to subclass this other one. We could make this collision component sub class the visual component, but if the user also subclasses this visual component, it wouldn't work because the class is not on the same level (unlike a simple diamond relationship, which is desired), and would give the cryptic meta class errors which are hard to understand for the programmer.

Therefore, I would like to know if there is any cool way, through maybe metaclass redefinition or using class decorators, to mark these unatomic components, and when they are subclassed, the additional dependency would be injected into the current object, if its not yet available. Example:

class AtomicComponent(object):
    pass

@depends(AtomicComponent)  # <- something like this?
class UnAtomicComponent(object):
    pass

class UserClass(UnAtomicComponent): #automatically includes AtomicComponent
    pass

class UserClass2(AtomicComponent, UnAtomicComponent): #also works without problem
    pass

Can someone give me an hint on how I can do this? or if it is even possible...

edit: Since it is debatable that the meta class solution is the best one, I'll leave this unaccepted for 2 days.

Other solutions might be to improve error messages, for example, doing something like UserClass2 would give an error saying that UnAtomicComponent already extends this component. This however creates the problem that it is impossible to use two UnAtomicComponents, given that they would subclass object on different levels.

1

There are 1 answers

7
jsbueno On BEST ANSWER

"Metaclasses"

This is what they are for! At time of class creation, the class parameters run through the metaclass code, where you can check the bases and change then, for example.

This runs without error - though it does not preserve the order of needed classes marked with the "depends" decorator:

class AutoSubclass(type):
    def __new__(metacls, name, bases, dct):
        new_bases = set()
        for base in bases:
            if hasattr(base, "_depends"):
                for dependence in base._depends:
                    if not dependence in bases:
                        new_bases.add(dependence)
        bases = bases + tuple(new_bases)
        return type.__new__(metacls, name, bases, dct)



__metaclass__ = AutoSubclass

def depends(*args):
    def decorator(cls):
        cls._depends = args
        return cls
    return decorator


class AtomicComponent:
    pass

@depends(AtomicComponent)  # <- something like this?
class UnAtomicComponent:
    pass

class UserClass(UnAtomicComponent): #automatically includes AtomicComponent
    pass

class UserClass2(AtomicComponent, UnAtomicComponent): #also works without problem
    pass

(I removed inheritance from "object", as I declared a global __metaclass__ variable. All classs will still be new style class and have this metaclass. Inheriting from object or another class does override the global __metaclass__variable, and a class level __metclass__ will have to be declared)

-- edit --

Without metaclasses, the way to go is to have your classes to properly inherit from their dependencies. Tehy will no longer be that "atomic", but, since they could not work being that atomic, it may be no matter.

In the example bellow, classes C and D would be your User classes:

>>> class A(object): pass
... 
>>> class B(A, object): pass
... 
>>> 
>>> class C(B): pass
... 
>>> class D(B,A): pass
... 
>>>