Mixins?
Python Asteroids+Invaders on GitHub
Are mixins a moral issue, or simply a useful tool?
Inheritance
Someone once said that inheritance is just a programmer’s hack for saving code, and there is truth to that saying. We need look no further than the program in front of us, our Python game supporting both Asteroids and Space Invaders.
On the Space Invaders side of things, we have a class InvadersFlyer which implements trivial pass
methods for all our interaction “events”. Since no object interacts with all the others, this allows each concrete object to implement only the interactions that it actually needs. This keeps each concrete object much simpler, since there are no fewer than 65 lines of those pass
methods.
There are those among my dear friends [COUGH Hill COUGH] who eschew the use of concrete inheritance and also avoid it, while not using it, nor recommending it to others. And there are certainly reasons for avoiding the practice. Perhaps the most significant is this:
When looking at the code for an object that inherits concrete behavior, it is not possible to figure out what it does without looking at all the classes from which it inherits.
All my pals and I believe strongly that the code we write should be very clear, expressing clearly what it does and how it does it. Inheritance of concrete behavior makes it much more difficult to see what the program does.
Now there is another valuable use for inheritance of abstract behavior: in languages that do compile-time type checking, it’s basically the only way to get polymorphism, the ability for different classes to have similar enough behavior that users of those classes can use any of them. In one of those languages, we define in a superclass just the names and calling sequences of the methods the class promises to implement, and then the compiler forces er, reminds us to implement those methods.
In languages like Python, with “dynamic” typing, also called “duck” typing, you can send any message to any object and if it understands that message, that code runs. The compiler, generally speaking, just assumes that you know what you’re doing, and at run time, if something goes wrong, you get a run-time error.
Now a run-time error is thousands of times worse than a compile-time error. With the latter, it bothers the programmer, who fixes the issue quickly. With the former, all the users of the program all over the world can be bothered by the error … and fixing it involves getting a new version of the software to everyone who uses it.
It’s no wonder that some people prefer languages with strict compile-time checking, and it’s no wonder that there are coming to be stronger and stronger analysis programs for Python that can do early checking for type errors.
I think it is fair to say that a compile-time type checker provides discipline to the software effort, and that if you use a duck-typing language, the programmer has to provide that discipline if you’re to have the same reliability. Programmers are mostly human, and discipline was not a feature that was prioritized during human evolution, so it is meet and just to be a bit fearful of building software in a duck-typing language.
Why then, do we do it? Software development in a duck-typing language can proceed faster than in a language with compile-time type checking, because—in most cases—those languages require programmers to jump through more hoops to get the job done.
But I’m not here to wage the language wars. I’ve used more programming languages than most people can name, and found them all to be interesting and capable of doing a good job of implementing what I needed to implement. I do prefer duck typing, but I recognize that as a preference, not some law of nature.
And today, I’m here to talk about:
Mixins
Mixins are a way of allowing us to bring some behavior into a class. Years ago, we used to use #include
to bring common code into our programs. Some languages still have that facility. Mixins are similar, but they use a language’s inheritance mechanism to do the job.
Let’s not try to define the thing abstractly. Let’s do one, and then we can speak of it more concretely.
Exploding
In the Space Invaders game, exploding seems to be popular, and we have a number of objects with strikingly similar methods:
class InvaderPlayer...
def explode(self, fleets):
frac = u.screen_fraction(self.position)
player.play_stereo("explosion", frac)
explosion = GenericExplosion.player_explosion(self.position, 1.0)
fleets.append(explosion)
fleets.remove(self)
class ReservePlayer...
def explode(self, fleets):
frac = u.screen_fraction(self.position)
player.play_stereo("explosion", frac)
explosion = GenericExplosion.player_explosion(self.position, 1.0)
fleets.append(explosion)
fleets.remove(self)
class RobotPlayer...
def explode(self, fleets):
frac = u.screen_fraction(self.position)
player.play_stereo("explosion", frac)
explosion = GenericExplosion.player_explosion(self.position, 1.0)
fleets.append(explosion)
fleets.remove(self)
class InvadersSaucer...
def explode_scream_and_die(self, fleets):
explosion = GenericExplosion.saucer_explosion(self.position, 0.5)
fleets.append(explosion)
fleets.append(InvaderScore(self._mystery_score()))
self.play_death_sound()
self._die(fleets)
class PlayerShot...
def explode(self, fleets):
fleets.append(GenericExplosion.shot_explosion(self.position, 0.125))
fleets.remove(self)
The first three of these are quite clearly the same. We’ll go after them first, then perhaps consider the other two.
We’ll create a new class, ExplosionMixin. Like this:
class ExplosionMixin:
def explode(self, fleets):
frac = u.screen_fraction(self.position)
player.play_stereo("explosion", frac)
explosion = GenericExplosion.player_explosion(self.position, 1.0)
fleets.append(explosion)
fleets.remove(self)
We have to tell PyCharm to ignore the fact that we have not declared position
in ExplosionMixin. Compile-time type checking, just a tiny bit. IDE time, actually. The compiler won’t care, but PyCharm does.
Now, in InvaderPlayer we do this:
class InvaderPlayer(ExplosionMixin, Spritely, InvadersFlyer):
And we remove the explode
method from InvaderPlayer. Tests went red when I removed the method and went back to green when I added the reference to the mixin, so I am confident that our tests would find any issues. And I try the game anyway.
All is well. Commit: Use new ExplosionMixin in InvaderPlayer.
Now, of course, we do the same with ReservePlayer. Again some tests break when we remove the local method and heal when we refer to the mixin.
Test. Green. Commit: Use ExplosionMixin in ReservePlayer.
Once more, for RobotPlayer. Test. Green. Commit: Use ExplosionMixin in RobotPlayer.
Perfect. Let’s reflect
Reflection
The “class” ExplosionMixin implements one method that can be, and now is used by three separate classes. There is a small price to pay. Consider the simplest of the three, ReservePlayer:
class ReservePlayer(ExplosionMixin, Spritely, InvadersFlyer):
@classmethod
def invalid(cls):
return cls(-666)
def __init__(self, reserve_number=0):
self._sprite = Sprite.player()
position_in_row = reserve_number * (5 * self.rect.width // 4)
x = u.INVADER_PLAYER_LEFT + position_in_row
self.position = Vector2(x, u.RESERVE_PLAYER_Y)
self.reserve_number = reserve_number
@property
def is_valid(self):
return self.reserve_number >= 0
def interact_with(self, other, fleets):
other.interact_with_reserveplayer(self, fleets)
def interact_with_destructor(self, destructor, fleets):
self.explode(fleets)
def rightmost_of(self, other_player):
return other_player if other_player.reserve_number > self.reserve_number else self
If we happen to be reading this class and we happen to wonder “how does it explode”, we cannot see at a glance how it does it. In PyCharm we would have to type Command+B, which would take us right here:
class ExplosionMixin:
def explode(self, fleets):
frac = u.screen_fraction(self.position)
player.play_stereo("explosion", frac)
explosion = GenericExplosion.player_explosion(self.position, 1.0)
fleets.append(explosion)
fleets.remove(self)
Not much of a hardship, really. Fact is, we often use Command+B just to find local methods anyway.
So that’s a mixin: a small class, not intended to be instantiated on its own, that is inherited by other classes so as to provide the methods of the mixin class.
Yes, this is inheritance of concrete behavior. But the mixin notion tells us that we are not saying “RobotPlayer is a kind of ExplosionMixin”, in the way that we would say “RobotPlayer is a kind of InvadersFlyer”.
Which brings us to an interesting question. Recall that these classes (and others) inherit Spritely.
class Spritely:
@property
def sprite(self):
return self._sprite
@property
def mask(self):
return self.sprite.mask
@property
def rect(self):
return self.sprite.rectangle
@property
def position(self):
return self.sprite.position
@position.setter
def position(self, vector):
self.sprite.position = vector
def colliding(self, flyer):
return self.sprite.colliding_with_flyer(flyer)
def draw(self, screen):
self.sprite.draw(screen)
Should Spritely be considered to be a mixin? I believe that it probably is. I gave it the name Spritely more or less intentionally, as a kind of “adjective”. We could call our ExplosionMixin “Explody” if that were a word. “Explodish”? I don’t know.
I do think that Spritely is a mixin. Let’s rename it accordingly.
class SpritelyMixin:
...
class ReservePlayer(ExplosionMixin, SpritelyMixin, InvadersFlyer):
...
And so on. Commit: rename Spritely to SpritelyMixin.
That was a nine-file commit by the way. PyCharm did all the work: I just typed Shift+F6 and the new class name. Whee!
Summary
So, there we have the “mixin”, a class, typically small, not intended to be instantiated, but only “included” into other classes to provide some kind of common behavior. Declaring them with “Mixin” in the class name alerts us to what’s going on in the classes that inherit the mixin, and our IDE makes seeing the implementations easy in the event that we need to. Generally speaking, once we see self.explode()
, we don’t even need to look further, except in the rare case that we want to change how the explosion works.
We might wonder about the other explode
methods and whether they should use our mixin:
class InvadersSaucer...
def explode_scream_and_die(self, fleets):
explosion = GenericExplosion.saucer_explosion(self.position, 0.5)
fleets.append(explosion)
fleets.append(InvaderScore(self._mystery_score()))
self.play_death_sound()
self._die(fleets)
class PlayerShot...
def explode(self, fleets):
fleets.append(GenericExplosion.shot_explosion(self.position, 0.125))
fleets.remove(self)
It looks to me as if we might be able to fiddle with the saucer and use our new mixin. The PlayerShot would be more of a reach. We have options. Tomorrow, we might look at those others, but the benefit doesn’t leap out at me. It might not be worth the effort. It looks like we’d have to parameterize the timing, the explosion name, and the sound. Still … I can imagine a way of doing that … but other than the exercise, I’m not really seeing the value.
Today, we found and removed a bit of duplication, just a few lines, but less clutter in three classes has some value. And now we have a bit better understanding of the mixin idea. I like it. What do you think?
See you next time!