5

Is there a reasonable way in Python to implement mixin behavior similar to that found in Ruby -- that is, without using inheritance?

class Mixin(object):
    def b(self): print "b()"
    def c(self): print "c()"

class Foo(object):
    # Somehow mix in the behavior of the Mixin class,
    # so that all of the methods below will run and
    # the issubclass() test will be False.

    def a(self): print "a()"

f = Foo()
f.a()
f.b()
f.c()
print issubclass(Foo, Mixin)

I had a vague idea to do this with a class decorator, but my attempts led to confusion. Most of my searches on the topic have led in the direction of using inheritance (or in more complex scenarios, multiple inheritance) to achieve mixin behavior.

FMc
  • 41,963
  • 13
  • 79
  • 132
  • 1
    Trying to combine classes like this will inevitably lead to the diamond problem of MI; even in the most basic case, all of the classes derive from `object`. This issue is already solved, and in a clearly documented way, when using MI in Python. Beware that any approach like this is going to have to deal with it all over again, and it'll inevitably be inconsistent with how standard Python inheritance handles it. (FWIW, I can't contrive any reason to want to do this--it's precisely what MI is for. What are you doing?) – Glenn Maynard Nov 09 '10 at 23:10
  • 3
    @Glenn Maynard: the whole point of mixin composition is that all the mixins get flattened at composition time into a linear inheritance chain, precisely to *avoid* diamonds. If you get a diamond, it's not really mixin composition, it's multiple inheritance. – Jörg W Mittag Nov 09 '10 at 23:33
  • 1
    @Mittag: There's a diamond involved in the composition, because you're mixing classes with a shared base class. There is, in any case, no need to avoid diamonds in Python, because Python already flattens the call chain for you, so I can't think of why you'd want to do this in Python to begin with. – Glenn Maynard Nov 10 '10 at 00:31
  • @Glenn Maynard Thanks for the comments, and I take your points. The question is driven mainly by curiousity: I have no specific use case at the moment. I've been learning both languages during the past two years and am simply trying to understand their different approaches more clearly. – FMc Nov 10 '10 at 13:17

8 Answers8

9
def mixer(*args):
    """Decorator for mixing mixins"""
    def inner(cls):
        for a,k in ((a,k) for a in args for k,v in vars(a).items() if callable(v)):
            setattr(cls, k, getattr(a, k).im_func)
        return cls
    return inner

class Mixin(object):
    def b(self): print "b()"
    def c(self): print "c()"

class Mixin2(object):
    def d(self): print "d()"
    def e(self): print "e()"


@mixer(Mixin, Mixin2)
class Foo(object):
    # Somehow mix in the behavior of the Mixin class,
    # so that all of the methods below will run and
    # the issubclass() test will be False.

    def a(self): print "a()"

f = Foo()
f.a()
f.b()
f.c()
f.d()
f.e()
print issubclass(Foo, Mixin)

output:

a()
b()
c()
d()
e()
False
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
  • Nitpick, since `inner` receives the class to decorate as its argument, don't you think `c` might have been a little better name? It's not a function decorator, after all. – martineau Nov 10 '10 at 12:38
  • It might be worth pointing out that if there are any method name collisions, the one from the last mixin class listed wins. With a little (more) ingenuity and code, it should be possible to arrange to have each one called in succession, although I'm not sure what the final return value ought to be -- a tuple? – martineau Nov 10 '10 at 12:47
  • 1
    @martineau, fair point, I changed it to `cls`. I think if you are having name collisions with your mixins you are asking for trouble - they are likely to have unrelated purposes and even different parameters. Perhaps a warning when there is a conflict is appropriate – John La Rooy Nov 10 '10 at 12:50
  • Wasn't really serious about calling both the like-named methods. Good idea to just add `if hasattr(cls, k): warnings.warn('{!r} method name conflict'.format(k))` before the `setattr(cls,...`. – martineau Nov 10 '10 at 20:22
  • @gnibber Thanks a lot. This looks useful and is easy to understand. I'd be curious to know whether there would be any advantages or disadvantages of this approach relative to the metaclass alternatives. Also, would you actually use/recommend this in any contexts, or would you simply achieve mixin behavior directly through inheritance? – FMc Nov 10 '10 at 23:59
4

You can add the methods as functions:

Foo.b = Mixin.b.im_func
Foo.c = Mixin.c.im_func
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
3

EDIT: Fixed what could (and probably should) be construed as a bug. Now it builds a new dict and then updates that from the class's dict. This prevents mixins from overwriting methods that are defined directly on the class. The code is still untested but should work. I'm busy ATM so I'll test it later. It worked fine except for a syntax error. In retrospect, I decided that I don't like it (even after my further improvements) and much prefer my other solution even if it is more complicated. The test code for that one applies here as well but I wont duplicate it.

You could use a metaclass factory:

 import inspect

 def add_mixins(*mixins):
     Dummy = type('Dummy', mixins, {})
     d = {}

     for mixin in reversed(inspect.getmro(Dummy)):
         d.update(mixin.__dict__)

     class WithMixins(type):
         def __new__(meta, classname, bases, classdict):
             d.update(classdict)
             return super(WithMixins, meta).__new__(meta, classname, bases, d)
     return WithMixins 

then use it like:

 class Foo(object):
     __metaclass__ = add_mixins(Mixin1, Mixin2)

     # rest of the stuff
Community
  • 1
  • 1
aaronasterling
  • 68,820
  • 20
  • 127
  • 125
3

I am not that familiar with Python, but from what I know about Python metaprogramming, you could actually do it pretty much the same way it is done in Ruby.

In Ruby, a module basically consists of two things: a pointer to a method dictionary and a pointer to a constant dictionary. A class consists of three things: a pointer to a method dictionary, a pointer to a constant dictionary and a pointer to the superclass.

When you mix in a module M into a class C, the following happens:

  1. an anonymous class α is created (this is called an include class)
  2. α's method dictionary and constant dictionary pointers are set equal to M's
  3. α's superclass pointer is set equal to C's
  4. C's superclass pointer is set to α

In other words: a fake class which shares its behavior with the mixin is injected into the inheritance hierarchy. So, Ruby actually does use inheritance for mixin composition.

I left out a couple of subleties above: first off, the module doesn't actually get inserted as C's superclass, it gets inserted as C's superclasses' (which is C's singleton class) superclass. And secondly, if the mixin itself has mixed in other mixins, then those also get wrapped into fake classes which get inserted directly above α, and this process is applied recursively, in case the mixed in mixins in turn have mixins.

Basically, the whole mixin hierarchy gets flattened into a straight line and spliced into the inheritance chain.

AFAIK, Python actually allows you to change a class's superclass(es) after the fact (something which Ruby does not allow you to do), and it also gives you access to a class's dict (again, something that is impossible in Ruby), so you should be able to implement this yourself.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • @aaronasterling: Thanks for the info. As I said, I don't know enough about the intricacies of Python metaprogramming. I *do* however know Ruby, so I thought I'd shed some light on how mixin composition *actually* works, especially in light of the OP's claim that it doesn't use inheritance. Anyway, it appears that it's not quite as straightforward as I thought to copy the Ruby approach 100%, but there is a Pythonic approach using metaclasses. Is that about right? – Jörg W Mittag Nov 09 '10 at 23:38
  • 1
    @aaronasterling: Yeah, that's a special case behavior in Ruby. Basically, method lookup and constant lookup follow the superclass pointer chain. The `class` and `superclass` methods do so, too, but they special case so-called *virtual classes* (singleton classes and include classes) and simply ignore them. IOW: mixins *use* inheritance, but they try their pretty darned hardest to conceal that fact. Basically, the choice was about complicating the method lookup algorithm by having two branches for classes and mixins or having only classes and instead complicate introspection. – Jörg W Mittag Nov 10 '10 at 00:10
  • örg Thanks, this is very instructive. – FMc Nov 10 '10 at 01:19
  • I looked at your description again and realized that most of my comments were garbage so I deleted them. I'm pretty sure that I've implemented what you're describing [here](http://stackoverflow.com/questions/4139508/in-python-can-one-implement-mixin-behavior-without-using-inheritance/4144261#4144261). – aaronasterling Nov 10 '10 at 12:14
3

This one is based on the way it's done in ruby as explained by Jörg W Mittag. All of the wall of code after if __name__=='__main__' is test/demo code. There's actually only 13 lines of real code to it.

import inspect

def add_mixins(*mixins):
    Dummy = type('Dummy', mixins, {})
    d = {}

    # Now get all the class attributes. Use reversed so that conflicts
    # are resolved with the proper priority. This rules out the possibility
    # of the mixins calling methods from their base classes that get overridden
    # using super but is necessary for the subclass check to fail. If that wasn't a
    # requirement, we would just use Dummy above (or use MI directly and
    # forget all the metaclass stuff).

    for base in reversed(inspect.getmro(Dummy)):
        d.update(base.__dict__)

    # Create the mixin class. This should be equivalent to creating the
    # anonymous class in Ruby.
    Mixin = type('Mixin', (object,), d)

    class WithMixins(type):
        def __new__(meta, classname, bases, classdict):
            # The check below prevents an inheritance cycle from forming which
            # leads to a TypeError when trying to inherit from the resulting
            # class.
            if not any(issubclass(base, Mixin) for base in bases):
                # This should be the the equivalent of setting the superclass 
                # pointers in Ruby.
                bases = (Mixin,) + bases
            return super(WithMixins, meta).__new__(meta, classname, bases,
                                                   classdict)

    return WithMixins 


if __name__ == '__main__':

    class Mixin1(object):
        def b(self): print "b()"
        def c(self): print "c()"

    class Mixin2(object):
        def d(self): print "d()"
        def e(self): print "e()"

    class Mixin3Base(object):
        def f(self): print "f()"

    class Mixin3(Mixin3Base): pass

    class Foo(object):
        __metaclass__ = add_mixins(Mixin1, Mixin2, Mixin3)

        def a(self): print "a()"

    class Bar(Foo):
        def f(self): print "Bar.f()"

    def test_class(cls):
        print "Testing {0}".format(cls.__name__)
        f = cls()
        f.a()
        f.b()
        f.c()
        f.d()
        f.e()
        f.f()
        print (issubclass(cls, Mixin1) or 
               issubclass(cls, Mixin2) or
               issubclass(cls, Mixin3))

    test_class(Foo)
    test_class(Bar)
Community
  • 1
  • 1
aaronasterling
  • 68,820
  • 20
  • 127
  • 125
  • Thanks for both of your answers -- very interesting. Why do you prefer this approach over your other answer? – FMc Nov 10 '10 at 13:12
  • BTW: [This](https://GitHub.Com/EvanPhx/Rubinius/blob/master/kernel/delta/module.rb#L63-L151) is Rubinius's implementation of mixin composition. I realize I completely forgot about `Module#append_features`. – Jörg W Mittag Nov 10 '10 at 13:19
  • @FM I prefer it because it does use inheritance. I 'feels' better. I know that's a lame answer but this is the sort of stuff that inheritance was designed for and even ruby ends up using it behind the scenes. – aaronasterling Nov 10 '10 at 13:20
0

You could decorate the classes __getattr__ to check in the mixin. The problem is that all methods of the mixin would always require an object the type of the mixin as their first parameter, so you would have to decorate __init__ as well to create a mixin-object. I believe you could achieve this using a class decorator.

Community
  • 1
  • 1
Björn Pollex
  • 75,346
  • 28
  • 201
  • 283
0
from functools import partial
class Mixin(object):
    @staticmethod
    def b(self): print "b()"
    @staticmethod
    def c(self): print "c()"

class Foo(object):
    def __init__(self, mixin_cls):
        self.delegate_cls = mixin_cls

    def __getattr__(self, attr):
        if hasattr(self.delegate_cls, attr):
            return partial(getattr(self.delegate_cls, attr), self)

    def a(self): print "a()"

f = Foo(Mixin)
f.a()
f.b()
f.c()
print issubclass(Foo, Mixin)

This basically uses the Mixin class as a container to hold ad-hoc functions (not methods) that behave like methods by taking an object instance (self) as the first argument. __getattr__ will redirect missing calls to these methods-alike functions.

This passes your simple tests as shown below. But I cannot guarantee it will do all the things you want. Make more thorough test to make sure.

$ python mixin.py 
a()
b()
c()
False
eddie_c
  • 3,403
  • 1
  • 16
  • 6
0

Composition? It seems like that would be the simplest way to handle this: either wrap your object in a decorator or just import the methods as an object into your class definition itself. This is what I usually do: put the methods that I want to share between classes in a file and then import the file. If I want to override some behavior I import a modified file with the same method names as the same object name. It's a little sloppy, but it works.

For example, if I want the init_covers behavior from this file (bedg.py)

import cove as cov


def init_covers(n):
    n.covers.append(cov.Cover((set([n.id]))))
    id_list = []
    for a in n.neighbors:
        id_list.append(a.id)
    n.covers.append(cov.Cover((set(id_list))))

def update_degree(n):
    for a in n.covers:
        a.degree = 0
        for b in n.covers:
            if  a != b:
                a.degree += len(a.node_list.intersection(b.node_list))    

In my bar class file I would do: import bedg as foo

and then if I want to change my foo behaviors in another class that inherited bar, I write

import bild as foo

Like I say, it is sloppy.

philosodad
  • 1,808
  • 14
  • 24