Dynamically add mixin to a class depends on attribute value in python

46 Views Asked by At

I want to dynamically add a mixin class in an object without effecting any other objects of that class. I have an external service that determine which mixin should be used for a particular object.

I am currently using a metaclass to dynamically add a base class to an object. But the issue is, if I change base class of any object, it effects all other objects as well. Here is a simplified sample code snippet:

class M(type):
    def __call__(cls, *args, **kwargs):
        obj = type.__call__(cls, *args, **kwargs)
        if hasattr(obj, "mixin"):
            obj.__class__.__bases__ = (obj.mixin,) + obj.__class__.__bases__
        return obj


class MixinA:
    name = "A"
    attr1 = 100


class MixinB:
    name = "B"
    attr2 = 200


class A(metaclass=M):
    pass


class B(A):
    def __init__(self, some_value):
        self.mixin = external_function(some_value)


b1 = B('X')  # Let's say it should use MixinA
print(b1.name, b1.__class__.__base__)

b2 = B('Y') # Let's say it should use MixinB
print(b2.name, b2.__class__.__base__)
print(b1.name, b1.__class__.__base__)

This will output:

A <class '__main__.MixinA'>
B <class '__main__.MixinB'>
B <class '__main__.MixinB'>

What can I do so that changing base class of b2 will not effect b1. So the output should be:

A <class '__main__.MixinA'>
B <class '__main__.MixinB'>
A <class '__main__.MixinA'>

Or is there any other ways where I can access/execute all the properties/methods of the appropriate mixin applied dynamically to that object.

Edit: Updated description and code snippet for more clarity.

2

There are 2 best solutions below

0
Ali Hasan Imam On BEST ANSWER

Getting the inspiration from @chepner comments and the solution proposed, I have 2 solutions. The idea is same as creating a new dynamic class that compose of both the original class and the mixin class.

Solution 1: Using Metaclass

class M(type):
  def __call__(cls, *args, **kwargs):
    obj = type.__call__(cls, *args, **kwargs)
    if hasattr(obj, "mixin"):
        new_cls = type(cls.__name__ + obj.mixin.__name__, (cls, obj.mixin), {})
        new_obj = type.__call__(new_cls, *args, **kwargs)
        return new_obj
    return obj

Solution 2: Using Factory

def factory(cls, *args, **kwargs):
  obj = cls(*args, **kwargs)
  if hasattr(obj, "mixin"):
    new_cls = type(cls.__name__ + obj.mixin.__name__, (cls, obj.mixin), {})
    new_obj = new_cls(*args, **kwargs)
    return new_obj
  return obj
4
chepner On

Instead of changing the base class of an existing object, define an appropriate subclass before instantiating B. This doesn't require a metaclass at all, just a factory function. Something like

class A:
    pass

class B(A):
    pass

class MixinA:
    pass

class MixinB:
    pass

_cache  = {}
def compose_classes(base, *mixins):
    t = (base, ) + mixins
    if t not in _cache:
        name = f'{base.__name__}With{''.join(x.__name__ for x in mixins)}'
        _cache[t] = type(name, t, {})
    return _cache[t]

b1 = compose_classes(B, MixinA)()
b2 = compose_classes(B, MixinB)()
b3 = compose_classes(B, MixinA)()        

b1, b2, and b3 are all instances of B, and b1 and b3 are instances of the same subclass of B. (I.e, type(..., (B, MixinA), {}) is only created once.)