Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I conditionally add in a mixin to the current class on instantiation?

I'd like to have a class that adds in mixins based on arguments passed to the constructor. This is what I've tried:

class MixinOne(object):
    def print_name(self):
        print("{} is using MixinOne.".format(self.name))


class MixinTwo(object):
    def print_name(self):
        print("{} is using MixinTwo.".format(self.name))


class Sub(object):

    def __new__(cls, *args, **kwargs):

        mixin = args[1]

        if mixin == 'one':
            bases = (MixinOne,) + cls.__bases__
        elif mixin == 'two':
            bases = (MixinTwo,) + cls.__bases__

        return object.__new__(type('Sub', bases, dict(cls.__dict__)))

    def __init__(self, name, mixin):

        print('In Sub.__init__')

        self.name = name

The only problem with this seems to be that __init__ doesn't get called, so the print_name methods will not work.

  1. How do I get __init__ on Sub to fire?

or

  1. Is there a better way to do this?
like image 427
moorepants Avatar asked Jan 28 '15 23:01

moorepants


2 Answers

This is a neat place to use metaclasses. You can put the custom-mixin-inclusion code in the meta, then your Sub classes don't need to have the boilerplate:

class AutoMixinMeta(type):
    def __call__(cls, *args, **kwargs):
        try:
            mixin = kwargs.pop('mixin')
            name = "{}With{}".format(cls.__name__, mixin.__name__)
            cls = type(name, (mixin, cls), dict(cls.__dict__))
        except KeyError:
            pass
        return type.__call__(cls, *args, **kwargs)

class Sub(metaclass = AutoMixinMeta):
    def __init__(self, name):
        self.name = name

Now you can create Sub objects and specify the mixin as follows:

>>> s = Sub('foo', mixin=MixinOne)
>>> s.print_name()
foo is using MixinOne.

It'll automatically get pulled out of the kwargs dict so the __init__ method can remain completely unaware of its existence.


Note: the metaclass declaration syntax in Python 2 is slightly different:

class Sub(object):
    __metaclass__ = AutoMixinMeta

    def __init__(self, name):
    self.name = name
like image 114
tzaman Avatar answered Nov 05 '22 11:11

tzaman


  1. __init__ is only called if __new__ returns an instance of the class. Your on-the-fly class inherits from a mixin, and from Sub's parents, but not from Sub itself. It'll probably work if you set bases = (MixinOne, cls).

  2. Write a factory function (or classmethod) instead of overloading Sub's construction. Even better, just make some subclasses instead of creating classes at runtime. :)

like image 38
Eevee Avatar answered Nov 05 '22 10:11

Eevee