6

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?
Raydel Miranda
  • 13,825
  • 3
  • 38
  • 60
moorepants
  • 1,813
  • 2
  • 22
  • 23

2 Answers2

10

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
tzaman
  • 46,925
  • 11
  • 90
  • 115
  • Nice solution. I went with the quick fix in Eevee's answer, but may refactor to use this. Thanks. – moorepants Jan 30 '15 at 18:44
  • I can't get the above code to work. I get a **TypeError** ```__init__() got an unexpected keyword argument 'mixin' ```. I tried adding `**kwargs` to the `__init__` method but without success – drstevok Jul 10 '17 at 14:09
  • @drstevok are you using Python 2 or 3? – tzaman Jul 10 '17 at 14:16
  • I am using Python 3.6.1. Here's the code https://gist.github.com/docsteveharris/7693d78e235b0d354cdc5b56ef264280 in case I made a typo – drstevok Jul 10 '17 at 14:17
  • 1
    @drstevok that would be it then. Python 3 doesn't look at the `__metaclass__` attribute, instead you need to provide it in the declaration. Try `class Sub(object, metaclass = AutoMixinMeta)` – tzaman Jul 10 '17 at 14:29
  • Ahhh. Thank you. That fixed it. – drstevok Jul 10 '17 at 15:23
3
  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. :)

Eevee
  • 47,412
  • 11
  • 95
  • 127
  • 2
    1. That works. Thanks. 2. I have quite a number of mixins and a number of classes like Sub, the array of subclasses I'd have to manually write doesn't warrant this solution to be practical. – moorepants Jan 28 '15 at 23:21