Welcome, guest | Sign In | My Account | Store | Cart

Using mixin classes via inheritance has its pros and cons. Here is an easy alternative via a decorator. As a bonus, you can mix in attributes from any object, not just classes.

Python, 35 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"""mixin module

"""

def add_mixin(cls, mixin, force=False):
    """Add the public attributes of a mixin to another class.

    Attribute name collisions result in a TypeError if force is False.
    If a mixin is an ABC, the decorated class is registered to it,
    indicating that the class implements the mixin's interface.

    """

    for name, value in mixin.__dict__.items():
        if name.startswith("_"):
            continue
        if not force and hasattr(cls, name):
            raise TypeError("name collision ({})".format(name))
        setattr(cls, name, value)
    try:
        mixin.register(cls)
    except AttributeError:
        pass
    

def mixes_in(*mixins, force=False):
    """A class decorator factory that adds mixins using add_mixin.

    """

    def decorator(cls):
        for mixin in mixins:
            add_mixin(cls, mixin, force)
        return cls
    return decorator

An interesting question is whether it is a good idea to allow arbitrary objects as mixins. One the one hand a non-class object may have attributes you want your class to have, like a container for some constants (see example 6). On the other hand it may not be obvious at first which attributes belong to the instance and which to the class (see example 7).

Here are some examples:

Example 1

from abc import ABCMeta

class PropMixin(metaclass=ABCMeta):
    @property
    def prop1(self): return "something"
    @staticmethod
    def do_static(): return 5
    @classmethod
    def do_class(cls): return cls


@mixes_in(PropMixin)
class X:
    NAME = "xray"

assert X.NAME == "xray"
assert X().prop1 == "something"
assert X.do_static() == 5
assert X.do_class() == X

Example 2

class MixinA(metaclass=ABCMeta):

    NAMES = ("A", "B")
    def do_unique(self, x): return x
    def do_A1(self): pass
    def do_A2(self): pass

class MixinB:
    def do_B1(self): pass
    def do_B2(self): pass
    def do_B3(self): pass

@mixes_in(MixinA, MixinB)
class Y: pass

assert Y().do_unique(5) == 5
assert Y.NAMES[0] == "A"
assert hasattr(Y, "do_A1")
assert hasattr(Y, "do_A2")
assert hasattr(Y, "do_B1")
assert hasattr(Y, "do_B2")
assert hasattr(Y, "do_B3")
assert issubclass(Y, MixinA)
assert not issubclass(Y, MixinB)

Example 3

class MixinFail:
    def do_unique(self): return "winner"

@mixes_in(MixinA, MixinFail, force=True)
class Z: pass

assert Z().do_unique() == "winner"

Example 4

@mixes_in(MixinA, MixinFail)
class Broken1: pass

# fails

Example 5

@mixes_in(MixinA)
class Broken2:
    NAMES = (1,2)

# fails

Example 6

flags = type("", (object,), {})()
flags.NOTSET = 0
flags.TALL = 1
flags.FAR = 2
flags.WIDE = 4
flags.DEEP = 8

@mixes_in(flags)
class Description: pass

assert Description.WIDE == 4

Example 7

class Typed:
    def get_type(self): return self.__class__

@mixes_in(X())
class UhOh: pass

assert UhOh.get_type() is Typed
# fails (the X instance does not have the get_type attribute to mix in)