pythoninheritancemixins

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


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.


Solution

  • 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