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.
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)