A Mixin?
Python Asteroids+Invaders on GitHub
We can remove some duplication with inheritance. One pythonic way is with a mixin. Let’s look into that.
All our classes that use the Sprite include the same code, which is copied into them:
@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
It would be nice if they didn’t have to do that. Each one will have to set the _sprite
member to be an instance of Sprite.
Python includes the notion of a “mixin”, which is a class from which methods can be inherited. The class is not in the general inheritance hierarchy of the using classes, but is “off to the side”. I am of a mind to try this, to see if I like it.
Could we just add a level to our hierarchy, under InvadersFlyer, and have our Sprite users inherit from that? In principle, we could, but as things stand now, the Invader, which does use a Sprite, does not inherit from InvadersFlyer. Invaders are not in the mix by themselves, instead they are managed by the InvaderGroup, so they need not be Flyers.
Digression
It could be argued that Invaders should be in the mix as flyers. My motivations for doing it otherwise were two:
-
Invaders do not move every cycle. Instead, only one invader moves in each cycle, giving them an eerie kind of rippling motion on the screen. I’m not saying there’s no way to do that with them in m=the mix, but I couldn’t think of a good one at the time.
-
The invaders do not interact with each other, or with anything other than the PlayerShot. It seemed inefficient to have them in the mix generating all those useless interaction calls. Yes, this could be seen as premature optimization, but given that I needed something to manage their motion anyway, we get this savings for free.
Be that as it may, the face is that the Invader is not a Flyer and so adding a SpritelyFlyer to the hierarchy would not help the Invaders. If we are to do this thing, we would like it to apply to all the objects that use Sprites.
I guess the thing is to try the idea and see whether we can make it work.
As a starting notion, I’ll just create a class that implements those methods.
class Spritely:
@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 draw(self, screen):
self._sprite.draw(screen)
I had to placate PyCharm about the references to _sprite
. Testing this class in InvaderSaucer by removing its own corresponding methods works just fine.
class InvadersSaucer(Spritely, InvadersFlyer):
def __init__(self, shot_count=0):
self.init_sprite()
self.init_position_and_speed(shot_count)
Similarly works with RoadFurniture. Similarly for InvaderShot. InvaderPlayer. PlayerShot.
This is going to work. I think I’ll start committing. Until now I was holding back until I was sure this would work.
Note that we could do this one class at a time if we chose. This means we could interleave these changes with other improvements and implementations.
Commit: installing Spritely.
Have I done all of them except for Invader? Help me, PyCharm. Yes, just Invader is left and Invader cheats:
@position.setter
def position(self, vector):
self._sprite.next_frame()
self.rect.center = vector + self.relative_position
We could let it override Spritely, but I think it’ll be better to give Invader a method that calls position with these extra frills. The method is actually called in InvaderGroup.
class InvaderGroup(InvadersFlyer):
def position_all_invaders(self, origin):
for invader in self.invaders:
invader.position = origin
def move_one_invader(self, origin):
invader = self.next_invader()
invader.position = origin
self._next_invader += 1
Let’s call the new method move_relative_to
, just to get off the dime here.
def position_all_invaders(self, origin):
for invader in self.invaders:
invader.move_relative_to(origin)
def move_one_invader(self, origin):
invader = self.next_invader()
invader.move_relative_to(origin)
self._next_invader += 1
I have to fix one test to use the move_relative_to
.
Commit: Invader inherits Spritely.
Summary
Although some would disagree, I find this to be OK. We have removed five boilerplate methods from each class that uses the Sprite, and the fact is represented rather clearly in the class definition by referring to Spritely as a separate superclass.
There is a hidden, secret, unwritten rule, which is that these classes need to have a _sprite
member, which is not presently checked. We might be able to write a test for that, but in any case the individual class tests will detect the problem if it were to occur.
I’ll do a bit more reading to see what folx do when they use this sort of thing.
I think this was worth doing. What do you think? Toot me up if you care to comment.
See you next time!