Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create a Mixin factory in Python?

I have a number of classes which are wrapped by other classes to add new functionality.

Unfortunately, the wrapper classes don't implement pass through functions for the classes they are wrapping, so the wrapper cannot be used interchangeably with the original class.

I would like to dynamically create classes that contain the functionality of both the wrapper and the original class.

The idea I had was to create a mix-in class and use a factory to apply it to the existing class to create a new dual use class dynamically. This should allow me to write the mix-in once, and for the mixed class to be used to provide either the original functionality or the enhanced functionality from from the mix-in via one object.

This is the sort of thing I'm after:

class A:
    def __init__(self):
        self.name = 'A'

    def doA(self):
        print "A:", self.name


class B(A):
    def __init__(self):
        self.name = 'B'

    def doB(self):
        print "B:", self.name


class C(A):
    def __init__(self):
        self.name = 'C'

    def doC(self):
        print "C:", self.name


class D:
    def doD(self):
        print "D:", self.name


class BD(B,D):
    pass


def MixinFactory(name, base_class, mixin):
    print "Creating %s" % name
    return class(base_class, mixin)     # SyntaxError: invalid syntax

a, b, c, d, bd = A(), B(), C(), D(), BD()

bd2 = MixinFactory('BD2', B, D)()
cd = MixinFactory('CD', C, D)()

a.doA()     # A: A

b.doA()     # A: B
b.doB()     # B: B

c.doA()     # A: C
c.doC()     # C: C

bd.doA()    # A: B
bd.doB()    # B: B
bd.doD()    # D: B

bd2.doA()   # A: B
bd2.doB()   # B: B
bd2.doD()   # D: B

cd.doA()    # A: C
cd.doC()    # C: C
cd.doD()    # D: C

The problem is that obviously that you can't just return a class from a function. Ignoring the syntax error though, the above code does show what I'm trying to achieve.

I had a play with the three argument variant of type() but couldn't get that to work, so I'm not sure if that is the right approach.

I assume that creating a mix-in factory of this type is possible in Python, so what do I need to understand to implement it?


As Niklas R commented, this answer to the question Python dynamic inheritance: How to choose base class upon instance creation? provides the solution to my query, but Ben's answer here provides a better explanation of why.

like image 686
Mark Booth Avatar asked Dec 17 '22 04:12

Mark Booth


2 Answers

Actually you can return a class from a function. Your syntax error is that you're using the class keyword as if it were a function you can invoke.

Remember that all a class block for is create a new class and then bind the name you chose to it (in the current scope). So just put a class block inside your function, then return the class!

def mixinFactory(name, base, mixin):
    class _tmp(base, mixin):
        pass
    _tmp.__name__ = name
    return _tmp

As noted by ejucovy, you can also call type directly:

def mixinFactory(name, base, mixin):
    return type(name, (base, mixin), {})

This works because it's (usually) what a class block actually does; it collects all the names you define in the class block into a dictionary, then passes the name of the class, a tuple of the base classes, and the dictionary on to type to construct the new class.

However, type is nothing more than the default metaclass. Classes are objects like everything else, and are instances of a class. Most classes are instances of type, but if there's another metaclass involved you should call that instead of type, just as you wouldn't call object to create a new instance of your class.

Your mixin (presumably) doesn't define a metaclass, so it should be compatible with any metaclass that is a subclass of type. So you can just use whatever the class of base is:

def mixinFactory(name, base, mixin):
    return base.__class__(name, (base, mixin), {})

However, S.Lott's comment really seems like the best "answer" to this question, unless your mixin factory is doing anything more complicated than just creating a new class. This is much clearer AND less typing than any of the dynamic class creation variants proposed:

class NewClass(base, mixin):
    pass
like image 69
Ben Avatar answered Dec 18 '22 16:12

Ben


You can have class declarations anywhere, and class declarations can refer to variables for subclassing. As for the class name, it's just the .__name__ attribute on the class object. So:

def MixinFactory(name, base_class, mixin):
  print "Creating %s" % name
  class kls(base_class, mixin):
    pass
  kls.__name__ = name
  return kls

should do it.

For a one-liner, the three-argument type function should work too:

def MixinFactory(name, base_class, mixin):
  return type(name, (base_class, mixin), {})
like image 44
ejucovy Avatar answered Dec 18 '22 16:12

ejucovy