2

I want the new class to dynamically inherit from different parents depending on an attribute given when creating an instance. So far I've tried something like this:

class Meta(type):
    chooser = None
    def __call__(cls, *args, **kwars):
        if kwargs['thingy'] == 'option':
            Meta.choose = option
        return super().__call__(*args, **kwargs)

    def __new__(cls, name, parents, attrs):
        if Meta.choose == option:
            bases = (parent1)
        return super().__new__(cls, name, parents, attrs)

It doesn't work, is there a way that, depending on one of the parameters of the instance, I can dynamically choose a parent for the class?

gramsch
  • 379
  • 1
  • 5
  • 18

1 Answers1

1

First, lets fix a trivial mistake in the code, and then dig into the "real problem": the bases parameter needs to be a tuple. when you do bases=(option) the right hand side is not a tuple - it is merely a parenthesized expression that will be resolved and passed on as the non-tuple option.

Change that to bases=(option,) whenever you need to create a tuple for the bases.

The second mistake is more conceptual, and is probably why you didn't get it to work across varios attempts: the __call__ method of the metaclass is not something one usually fiddles with. To keep a long history short, the __call__ method of a __class__ is what is called to coordinate the calling of the __new__ and __init__ methods of instances of that class - that is made by Python automatically, and it is the __call__ from type that has this mechanism. When you transpose that to your metaclass you might realise that the __call__ method of your metaclass is not used when the __new__ and __init__ methods of your metaclass itself are about to be called (when a class is defined). In other words - the __call__ that is used is on the "meta-meta" class (which is again, type).

The __call__ method you wrote will instead be used by the instances of your custom classes when they are created (and this is what you intended), and will have no effect on class creation as it won't invoke the metaclass' __new__ - just the class __new__ itself. (and this is not what you intended).

So, what you need is, from inside __call__ not to call super().__call__ with the same arguments you received: that will pass cls to type's call, and the bases for cls are baked in when the metaclass __new__ was run - it just happens when the class body itself is declared.

You must have, in this __call__ dynamically create a new class, or use one of a pre-filled in table, and them pass that dynamically created class to the type.__call__.

But - in the end of the day, one can perceive that all this can be done with a factory function, so there is no need to create this super-complicated meta-class mechanism for that - and other Python tools such as linters and static analysers (as embbeded in an IDE you or your colleagues may be using) might work better with that.

Solution using factory function:

def factory(cls, *args, options=None, **kwargs):
   
   if options == 'thingy': 
        cls = type(cls.__name__, (option1, ), cls.__dict__)
   elif options = 'other':
        ...
   return cls(*args, **kwargs)

If you don't want to create a new class on every call, but want to share a couple of pre-existing classes with common bases, just create a cache-dictionary, and use the dict's setdefault method:

class_cache = {}

def factory(cls, *args, options=None, **kwargs):
   
   
   if options == 'thingy': 
        cls = class_cache.setdefault((cls.__name__, options),
              type(cls.__name__, (option1, ), cls.__dict__))
   elif options = 'other':
        ...
   return cls(*args, **kwargs)

(The setdefault method will store the second argument on the dict if the key (name, options) does not exist yet).

using a metaclass:

updated

After breakfast :-) I came up with this: make your metaclass __new__ inject a __new__ function on the created class itself that will either create a new class, with the desired bases dynamically or used a cached one for the same options. But unlike the other example, use a metaclass to anotate the original parameters to the class creation to create the derived class:

parameter_buffer = {}
derived_classes = {}

class Meta:
    def __new__(metacls, name, bases, namespace):
        cls = super().__new__(metacls, name, bases, namespace)
        parameter_buffer[cls] = (name, bases, namespace) 
        def __new__(cls, *args, option=None, **kwargs):
            if option is None:
                return original_new(cls, *args, **kwargs)
            name, bases, namespace = parameter_buffer[cls]
            if option == 'thingy':
                bases = (option1,)
            elif option== 'thingy2':
                ...
            if not (cls, bases) in derived_classes:
                derived_classes[cls, bases] = type(name, bases, namespace)
            return derived_classes[cls, bases](*args, **kwargs)
            
        cls.__new__ = __new__
        return cls

To keep the example short, this simply overwrites any explict __new__ method on the class that uses this metaclass. Also, the derived classes created this way are not themselves bearer of the same capability since they are created calling type and the metaclass is discarded in the process. Both things could be taken care off by writing more careful code but it would become complicated as an example here.

Community
  • 1
  • 1
jsbueno
  • 99,910
  • 10
  • 151
  • 209