Python Mixins

Steven D'Aprano steve+comp.lang.python at pearwood.info
Sat Sep 24 04:57:01 EDT 2011


Matt wrote:

> I'm curious about what people's opinions are about using mixins in
> Python. I really like, for example, the way that class based views
> were implemented in Django 1.3 using mixins.

Mixins considered harmful:
http://www.artima.com/weblogs/viewpost.jsp?thread=246341
http://www.artima.com/weblogs/viewpost.jsp?thread=246483

Mixins are much too easy to abuse, but there are uses for them.

I have found mixins useful for testing. If I have a bunch of functions that
take mostly similar unit tests, I create a mixin for the common unit tests,
then apply them to the tests.

Suppose I have two functions, ham and spam, which are unrelated but do share
some common behaviours. Since they are unrelated, I don't want the spam
test suite to inherit from the ham test suite, or vice versa, but I don't
want to write the same tests multiple times. (Especially when I add cheese,
egg and tomato functions.) So I put the common behaviours in a mixin class:

class StandardTestMixin:
    def test_requires_one_argument(self):
        self.assertRaises(TypeError, self.func)
    def test_has_docstring(self):
        self.assertTrue(self.func.__doc__)

class TestHam(unittest.TestCase, StandardTestMixin):
    ...

class TestSpam(unittest.TestCase, StandardTestMixin):
    ...


In this case, I simply don't care that the use of a mixin implies that
TestHam is a StandardTestMixin. I treat that as an accident of
implementation: it makes no practical difference, since I write no code
that depends on isinstance(instance, StandardTestMixin).



[...]
> In terms of code, lets say we have the following classes:
> 
> class Animal
> class Yamlafiable
> class Cat(Animal, Yamlafiable)
> class Dog(Animal, Yamlafiable)
> 
> I've got an Animal that does animal things, a Cat that does cat things
> and a Dog that does dog things. I've also got a Yamlafiable class that
> does something clever to generically convert an object into Yaml in
> some way. Looking at these classes I can see that a Cat is an Animal,
> a Dog is an Animal, a Dog is not a Cat, a Cat is not a Dog, a Dog is a
> Yamlafiable? and a Cat is a Yamlafiable? Is that really true?

That depends on what you want.

Python says is that issubclass(Cat, Yamlafiable) will return True. The
*interpretation* of that fact is entirely up to you. If you want to
interpret it as meaning that cats are Yamlafiables, go right ahead. If you
want to interpret it as a technical result with no semantic meaning, that's
fine too. After all, just because "ram" in "programming" returns True
doesn't actually mean anything about male sheep and computing.

The map is not the territory, and object-oriented ancestor/descendant
relationships are not the same as real life relationships. It's a tool,
nothing more, and if the metaphor starts to creak at the edges, oh well, so
much for the metaphor.

In fact, even in real life, the ancestor/descendant metaphor creaks. What
should we make of bacteria which exchange DNA with other species of
bacteria? Which is the ancestor and which is the descendant? How about
symbionts like lichen? What about when we splice genes from one species
into another? So even in real life, there are multiple inheritance and
mixins.



> If my 
> objects are categorized correctly, in the correct inheritance
> hierarchy shouldn't that make more sense? Cats and Dogs aren't
> Yamlafiable, that doesn't define what they are, rather it defines
> something that they can do because of things that they picked up from
> their friend the Yamlafile.

The standard metaphor for inheritance in OOP doesn't include "learning
things from a friend". It is a very simple metaphor: everything is
either "is-a" or "has-a". If you inherit from a class, the is-a
relationship holds. If you don't want that, you can use composition
instead:

class Cat(Animal):
    def __init__(self, yamlifier):
        self.yamlifier = yamlifier
    def make_yaml(self):
        self.yamlifier.make_yaml(self, self.spam)

Now we say that a Cat has-a Yamlafiable.


> This is just a ridiculous example, but I think it is valid to say that
> these things shouldn't be limited to inherit functionality only from
> their parents, that they can pick other things up along the way as
> well. Which is easy to do, right?
> 
> Dog.something_new = something_new

And that works in Python. But the thing is, which would you rather write?

#1
class Dog(Animal, Yamlafiable):
    pass

or this?

#2
class Dog(Animal):
    pass

Dog.__dict__.update(Yamlafiable.__dict__)


But wait! The two aren't the same. There are quite big differences in
behaviour between inheritance and attribute injection:

* In the case of inheritance, attributes defined by Animal take 
  precedence over those defined by Yamlafiable. In the case of 
  injection, the opposite is the case.

* In the case of inheritance, attributes defined by Yamlafiable's
  superclasses are reachable. In the case of injection, they 
  aren't (at least not without more work).

* Inheritance is late-binding, injection is early-binding: if 
  you dynamically add a new method to Yamlafiable, Dog will 
  see the change in the first case but not the second.


That's not to say that one is right and the other is wrong. They're just
different. Use whichever tool you prefer.

Another option is straits:

http://www.artima.com/weblogs/viewpost.jsp?thread=246488



-- 
Steven




More information about the Python-list mailing list