Search code examples
pythoninheritancemixins

How to apply a "mixin" class to an old-style base class


I've written a mixin class that's designed to be layered on top of a new-style class, for example via

class MixedClass(MixinClass, BaseClass):
    pass

What's the smoothest way to apply this mixin to an old-style class? It is using a call to super in its __init__ method, so this will presumably (?) have to change, but otherwise I'd like to make as few changes as possible to MixinClass. I should be able to derive a subclass that makes the necessary changes.

I'm considering using a class decorator on top of a class derived from BaseClass, e.g.

@old_style_mix(MixinOldSchoolRemix)
class MixedWithOldStyleClass(OldStyleClass)

where MixinOldSchoolRemix is derived from MixinClass and just re-implements methods that use super to instead use a class variable that contains the class it is mixed with, in this case OldStyleClass. This class variable would be set by old_style_mix as part of the mixing process.

old_style_mix would just update the class dictionary of e.g. MixedWithOldStyleClass with the contents of the mixin class (e.g. MixinOldSchoolRemix) dictionary.

Is this a reasonable strategy? Is there a better way? It seems like this would be a common problem, given that there are numerous available modules still using old-style classes.


Solution

  • This class variable would be set by old_style_mix as part of the mixing process.

    ...I assume you mean: "...on the class it's decorating..." as opposed to "on the class that is its argument" (the latter would be a disaster).

    old_style_mix would just update the class dictionary of e.g. MixedWithOldStyleClass with the contents of the mixin class (e.g. MixinOldSchoolRemix) dictionary.

    No good -- the information that MixinOldSchoolRemix derives from MixinClass, for example, is not in the former's dictionary. So, old_style_mix must take a different strategy: for example, build a new class (which I believe has to be a new-style one, because old-style ones do not accept new-style ones as __bases__) with the appropriate sequence of bases, as well as a suitably tweaked dictionary.

    Is this a reasonable strategy?

    With the above provisos.

    It seems like this would be a common problem, given that there are numerous available modules still using old-style classes.

    ...but mixins with classes that were never designed to take mixins are definitely not a common design pattern, so the problem isn't common at all (I don't remember seeing it even once in the many years since new-style classes were born, and I was actively consulting, teaching advanced classes, and helping people with Python problems for many of those years, as well as doing a lot of software development myself -- I do tend to have encountered any "reasonably common" problem that people may have with features which have been around long enough!-).

    Here's example code for what your class decorator could do (if you prefer to have it in a class decorator rather than directly inline...):

    >>> class Mixo(object):
    ...   def foo(self):
    ...     print 'Mixo.foo'
    ...     self.thesuper.foo(self)
    ... 
    >>> class Old:
    ...   def foo(self):
    ...     print 'Old.foo'
    ... 
    >>> class Mixed(Mixo, Old):
    ...   thesuper = Old
    ... 
    >>> m = Mixed()
    >>> m.foo()
    Mixo.foo
    Old.foo
    

    If you want to build Mixed under the assumed name/binding of Mixo in your decorator, you could do it with a call to type, or by setting Mixed.__name__ = cls.__name__ (where cls is the class you're decorating). I think the latter approach is simpler (warning, untested code -- the above interactive shell session is a real one, but I have not tested the following code):

    def oldstylemix(mixin):
        def makemix(cls):
            class Mixed(mixin, cls):
                thesuper = cls
            Mixed.__name__ = cls.__name__
            return Mixed
        return makemix