Python 69 - TIL. Well, YIL.
I learned something new, and apply it. Also I have an idea of my own. Will wonders never cease!?!?
Yesterday I found mathspp.com, the site of Rodrigo Girão Serrão from Portugal. Rodrigo’s blog is excellent and his series of articles entitled “Pydon’ts” is particularly good. I’ve already learned some useful things and have bought him a pizza. Thanks, Rodrigo! So …
Yesterday I learned …
One of the articles I read was Truthy, Falsy, and bool. The first thing I learned was that when Python goes to decide whether an object is Truthy or Falsy, it does so first by trying to call bool
to see whether the object implements __bool__
, and then calling len
to see if it implements __len__
. If either of those returns a True/False, it is used. Otherwise the object is Truthy.
I was looking for information on __bool__
because in earlier versions of Asteroids, before I had my own Fleet collections, I had used if saucers
on a raw list of saucers to determine whether there were any saucers, because Python treats empty lists and sequence as Falsy. When I first did Fleet, I didn’t know about bool
, but I did know about len
, so I implemented __len__
:
class Fleet:
def __len__(self):
return len(self.flyers)
That allowed me to write, e.g.:
class ShipFleet(Fleet):
def tick(self, delta_time, fleets):
ships = fleets.ships
if len(ships) == 0:
self.ship_timer.tick(delta_time, fleets)
super().tick(delta_time, fleets)
return True
What I did not know was that implementing len
automatically adds in the truthy check, allowing this:
def tick(self, delta_time, fleets):
ships = fleets.ships
if not ships:
self.ship_timer.tick(delta_time, fleets)
super().tick(delta_time, fleets)
return True
Or even this:
def tick(self, delta_time, fleets):
if not fleets.ships:
self.ship_timer.tick(delta_time, fleets)
super().tick(delta_time, fleets)
return True
Just a bit nicer. Of course it will be even nicer, I think, if we implement bool
by implementing __bool__
:
def __bool__(self):
return bool(self.flyers)
There are many ways I could implement this, checking len > 0 etc etc, but I think I like this one best.
Now that I’ve done that, I’ll change some other calls to len
and then commit.
def safe_to_emerge(self):
if self.missiles:
return False
if self.saucer_missiles:
return False
return self.all_asteroids_are_away_from_center()
Commit: implement bool on Fleet and use it throughout.
Reflection
Is this better? In think that in Python terms, it definitely is. In other languages, empty collections generally do not turn out to be Falsy, but Python is consistent about “useless” values being Falsy, and so the usage is pythonic, as they say.
Is it “efficient”? Well, it’s as efficient as anything is likely to be. We could implement is_empty
on our Fleet, maybe like this:
def is_empty(self):
return len(self.flyers) > 0
But does that save even one lookup? I’m not sure. Better to program in the Python style, I think.
So, thanks again to Rodrigo. I expect to learn more good things as I cruise through his blog and Pydon’ts.
An Idea
I was thinking about the specialized Fleet classes that we already have, AsteroidFleet, SaucerFleet, and ShipFleet. It seemed to me to be “interesting” that we do not also have MissileFleet, since we have all the others. And that got me thinking about what we do with missiles and how a MissileFleet class might be helpful.
There are two special rules for missiles. A ship can have no more than four missiles flying at once, and a saucer can have only two. Ship does this:
class Ship:
def fire_if_possible(self, missiles):
if self.can_fire and len(missiles) < u.MISSILE_LIMIT:
missiles.append(Missile.from_ship(self.missile_start(), self.missile_velocity()))
self.can_fire = False
Saucer does it a bit differently:
class Saucer:
def fire_if_missile_available(self, saucer_missiles, ships):
if self.a_missile_is_available(saucer_missiles):
self.fire_a_missile(saucer_missiles, ships)
return True
else:
return False
@staticmethod
def a_missile_is_available(saucer_missiles):
return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT
def fire_a_missile(self, saucer_missiles, ships):
saucer_missiles.append(self.create_missile(ships))
def create_missile(self, ships):
should_target = random.random()
random_angle = random.random()
return self.suitable_missile(should_target, random_angle, ships)
...
Saucer goes on, since it can target missiles and whatnot, but that’s enough for now.
Both of these implementations check to see whether their missile collection has room for another missile, and if so, they create a missile and append it to the missile collection.
It seems that the collection could be more helpful. Suppose that …
- MissileFleet (a new class) knew a maximum number of missiles it could contain, and
- it had a method
fire_if_possible
that took a callback, and - the callback created and returned a missile, and
- the Fleet appended the missile.
It seems to me that this scheme would have some advantages. Only the MissileFleet would need to know about the limit. Limit checking would be removed from Ship and Saucer, as would appending the missile. Ship and Sauce could just concern themselves with creating a suitable missile, and that method would only be called when the missile was about to be put into use.
I think it’ll be better. I’m gonna try it. And I’m gonna TDD the object.
def test_missile_fleet(self):
fleet = MissileFleet([])
That’s enough to drive out the class:
class MissileFleet(Fleet):
def __init__(self, flyers):
super().__init__(flyers)
However, I want to have a maximum number of missiles. I make the test more aggressive:
def test_missile_fleet(self):
missiles = []
fleet = MissileFleet(missiles, 3)
fired = fleet.fire(lambda: 666 )
assert fired
assert len(missiles) == 1
assert missiles[0] == 666
And I implement a bit more than I need:
class MissileFleet(Fleet):
def __init__(self, flyers, maximum_number_of_missiles):
self.maximum_number_of_missiles = maximum_number_of_missiles
super().__init__(flyers)
def fire(self, callback):
if len(self) < self.maximum_number_of_missiles:
self.append(callback())
return True
else:
return False
And I complete the test:
def test_missile_fleet(self):
missiles = []
fleet = MissileFleet(missiles, 3)
fired = fleet.fire(lambda: 666 )
assert fired
assert len(missiles) == 1
assert missiles[-1] == 666
fired = fleet.fire(lambda: 777 )
assert fired
assert len(missiles) == 2
assert missiles[-1] == 777
fired = fleet.fire(lambda: 888 )
assert fired
assert len(missiles) == 3
assert missiles[-1] == 888
fired = fleet.fire(lambda: 999 )
assert not fired
assert len(missiles) == 3
It fires three, appends the returned values, and returns True when it fires and False when it doesn’t. Perfect. Commit: TDD MissileFleet with maximum number of missiles, and fire
method with callback.
Now let’s use the class in the game.
As things stand, we create Fleets like this:
class Game:
# noinspection PyAttributeOutsideInit
def init_fleets(self):
asteroids = []
missiles = []
saucers = []
saucer_missiles = []
self.ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
ships = []
self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)
We’re passing in empty collections, and then Fleets tucks them away:
class Fleets:
def __init__(self, asteroids, missiles, saucers, saucer_missiles, ships):
self.fleets = (
AsteroidFleet(asteroids),
Fleet(missiles),
SaucerFleet(saucers),
Fleet(saucer_missiles),
ShipFleet(ships))
So we can just change that:
class Fleets:
def __init__(self, asteroids, missiles, saucers, saucer_missiles, ships):
self.fleets = (
AsteroidFleet(asteroids),
MissileFleet(missiles, u.MISSILE_LIMIT),
SaucerFleet(saucers),
MissileFleet(saucer_missiles, u.SAUCER_MISSILE_LIMIT),
ShipFleet(ships))
Commit: Missile Fleets now use MissileFleet class with proper limit set.
Now let’s see about using this new facility.
I think Ship will be easier.
def fire_if_possible(self, missiles):
if self.can_fire and len(missiles) < u.MISSILE_LIMIT:
missiles.append(Missile.from_ship(self.missile_start(), self.missile_velocity()))
self.can_fire = False
Let’s extract a method first:
def fire_if_possible(self, missiles):
if self.can_fire and len(missiles) < u.MISSILE_LIMIT:
missiles.append(self.create_missile())
self.can_fire = False
def create_missile(self):
return Missile.from_ship(self.missile_start(), self.missile_velocity())
Now can’t we just say this:
def fire_if_possible(self, missiles):
if self.can_fire and missiles.fire(self.create_missile):
self.can_fire = False
Two tests fail, but the game works. The first test says:
def test_missile_timeout(self):
ship = Ship(Vector2(100,100))
missiles = []
ship.fire_if_possible(missiles)
assert len(missiles) == 1
missile = missiles[0]
missile.tick_timer(0.5, missiles)
assert len(missiles) == 1
missile.tick_timer(3.0, missiles)
assert len(missiles) == 0
I think this will help:
def test_missile_timeout(self):
ship = Ship(Vector2(100,100))
missiles = MissileFleet([], 4)
ship.fire_if_possible(missiles)
assert len(missiles) == 1
missile = missiles[0]
missile.tick_timer(0.5, missiles)
assert len(missiles) == 1
missile.tick_timer(3.0, missiles)
assert len(missiles) == 0
That passes, and it’s a better test since it was passing in a raw collection, no longer suitable.
def test_firing_limit(self):
ship = Ship(u.CENTER)
count = 0
missiles = []
while len(missiles) < u.MISSILE_LIMIT:
ship.can_fire = True
ship.fire_if_possible(missiles)
count += 1
assert len(missiles) == count
assert len(missiles) == u.MISSILE_LIMIT
ship.can_fire = True
ship.fire_if_possible(missiles)
assert len(missiles) == u.MISSILE_LIMIT
I think the same fix should work. Yes:
def test_firing_limit(self):
ship = Ship(u.CENTER)
count = 0
missiles = MissileFleet([], u.MISSILE_LIMIT)
while len(missiles) < u.MISSILE_LIMIT:
ship.can_fire = True
ship.fire_if_possible(missiles)
count += 1
assert len(missiles) == count
assert len(missiles) == u.MISSILE_LIMIT
ship.can_fire = True
ship.fire_if_possible(missiles)
assert len(missiles) == u.MISSILE_LIMIT
Much improved. Commit: Ship now uses MissileFleet.fire to fire missiles.
Let’s see whether there’s anything unused in Ship now. I think not. Right. The can_fire
flag handling is a bit odd, though:
def control_motion(self, delta_time, missiles):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_f]:
self.turn_left(delta_time)
if keys[pygame.K_d]:
self.turn_right(delta_time)
if keys[pygame.K_j]:
self.power_on(delta_time)
else:
self.power_off()
if keys[pygame.K_k]:
self.fire_if_possible(missiles)
else:
self.not_firing()
def not_firing(self):
self.can_fire = True
def fire_if_possible(self, missiles):
if self.can_fire and missiles.fire(self.create_missile):
self.can_fire = False
I think we should either all set the can_fire
flag directly, or all set it indirectly. I choose directly, and remove the not_firing
method.
def control_motion(self, delta_time, missiles):
if not pygame.get_init():
return
keys = pygame.key.get_pressed()
if keys[pygame.K_f]:
self.turn_left(delta_time)
if keys[pygame.K_d]:
self.turn_right(delta_time)
if keys[pygame.K_j]:
self.power_on(delta_time)
else:
self.power_off()
if keys[pygame.K_k]:
self.fire_if_possible(missiles)
else:
self.can_fire = True
Commit: remove not_firing
, set can_fire
flag directly. Questionable?
That went smoothly. Let’s look at Saucer. When the saucer timer ticks down, it does this:
def set_firing_timer(self):
self.missile_timer = Timer(u.SAUCER_MISSILE_DELAY, self.fire_if_missile_available)
def fire_if_missile_available(self, saucer_missiles, ships):
if self.a_missile_is_available(saucer_missiles):
self.fire_a_missile(saucer_missiles, ships)
return True
else:
return False
@staticmethod
def a_missile_is_available(saucer_missiles):
return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT
def fire_a_missile(self, saucer_missiles, ships):
saucer_missiles.append(self.create_missile(ships))
It seems that we can almost do this:
def fire_if_missile_available(self, saucer_missiles, ships):
return saucer_missiles.fire(self.create_missile)
The problem is that we need ships
in create_missile
. So we need to pass an additional parameter to fire
. Or any number? I’ll change the code to what needs to work:
def fire_if_missile_available(self, saucer_missiles, ships):
return saucer_missiles.fire(self.create_missile, ships)
Now I do have a test failing so let’s fix that, until we get the error I expect:
def test_can_only_fire_two(self):
saucer = Saucer()
saucer_missiles = MissileFleet([], u.SAUCER_MISSILE_LIMIT)
saucer.fire_if_possible(delta_time=0.1, saucer_missiles=saucer_missiles, ships=[])
assert not saucer_missiles
saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
assert len(saucer_missiles) == 1
saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
assert len(saucer_missiles) == 2
saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
assert len(saucer_missiles) == 2
I put in the reference to MissileFleet, and now the failure is:
def fire_if_missile_available(self, saucer_missiles, ships):
> return saucer_missiles.fire(self.create_missile, ships)
E TypeError: MissileFleet.fire() takes 2 positional arguments but 3 were given
That’s the error I want. Let’s add a direct test for the parameter.
def test_missile_fleet_parameter(self):
missiles = []
fleet = MissileFleet(missiles, 2)
fired = fleet.fire(lambda m: m*2, 333)
assert fired
assert len(missiles) == 1
assert missiles[-1] == 666
This version of fire is taking a parameter and multiplying it by 2 in the function. Test passes, because fire
is now:
def fire(self, callback, *args):
if len(self) < self.maximum_number_of_missiles:
self.append(callback(*args))
return True
else:
return False
And the other test runs as well. Game also looks good.
Let’s see if we can clean up anything extra from Saucer. Yes. These are unused:
@staticmethod
def a_missile_is_available(saucer_missiles):
return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT
def fire_a_missile(self, saucer_missiles, ships):
saucer_missiles.append(self.create_missile(ships))
The job of deciding to fire is now down to ticking the timer and asking the saucer_missiles to call us if we can fire:
def set_firing_timer(self):
self.missile_timer = Timer(u.SAUCER_MISSILE_DELAY, self.fire_if_missile_available)
def fire_if_possible(self, delta_time, saucer_missiles, ships):
self.missile_timer.tick(delta_time, saucer_missiles, ships)
def fire_if_missile_available(self, saucer_missiles, ships):
return saucer_missiles.fire(self.create_missile, ships)
def create_missile(self, ships):
should_target = random.random()
random_angle = random.random()
return self.suitable_missile(should_target, random_angle, ships)
The create_missile
and suitable_missile
methods are still carrying weight, since they decide whether to aim or not, and so on.
Reflection
Saucer concerns itself only with wanting to fire and trying to fire until firing returns True, resetting the timer, and with creating a suitable missile to fire when the time comes.
What is a bit obscure, I think, is that fire_if_possible
must return True if it fires and False if it does not, because the timer will only reset if we report success back.
Maybe we should annotate some of these methods:
def fire_if_missile_available(self, saucer_missiles, ships) -> bool:
return saucer_missiles.fire(self.create_missile, ships)
def fire(self, callback, *args) -> bool:
if len(self) < self.maximum_number_of_missiles:
self.append(callback(*args))
return True
else:
return False
I’d like to specify the callable in Timer but I can’t work out how to say that action
is a Callable with a return of None or bool. I’ve got this much:
class Timer:
def __init__(self, delay, action: Callable, *args):
self.delay = delay
self.action = action
self.args = args
self.elapsed = 0
A comment might be useful. I’ll do this:
class Timer:
def __init__(self, delay, action: Callable, *args):
"""action is callable returning None or bool"""
self.delay = delay
self.action = action
self.args = args
self.elapsed = 0
Better. I don’t generally comment much, but the docstring feature is pretty nice and perhaps useful on somewhat tricky classes like Timer.
After more study, I get this:
class Timer:
def __init__(self, delay, action: Callable[[...], Union[None, bool]], *args):
"""action is callable returning None or bool"""
self.delay = delay
self.action = action
self.args = args
self.elapsed = 0
That mess:
Callable[[...], Union[None, bool]]
I think it means that action
is callable with a list of arguments, returning either None
or bool
. I’ll leave it that way until it causes me trouble, which it probably never will because Python doesn’t check these and PyCharm will be nice about it if it checks. I don’t know if it does or not.
I’d call that semi-useful. The comment is more useful.
Commit: annotate a few returns around timers and firing.
Let’s sum up.
Summary
Learning a bit more about Truthy, Falsy, len
and bool
let me simplify some code.
More important, once again, creating a container class, MissileFleet, specific to fleets of missiles, paid off. It let us add a limit to the size of the collection to the MissileFleet class, and that let us move determination of whether to fire or not inside the MissileFleet, and by giving the fire
method a callback, we simplified both Ship and Saucer quite nicely.
We observe that callbacks, such as the one in MissileFleet.fire
and the one in Timer, are a bit less obvious than most code, and we tried annotating the methods with returns, to help with that. We might also do this:
def create_missile(self, ships):
"""callback method, called from saucer_missiles.fire"""
should_target = random.random()
random_angle = random.random()
return self.suitable_missile(should_target, random_angle, ships)
Can’t hurt, might help. Commit: add docstring comment.
All this polishing is paying off in two regards. It’s making the Asteroids program more like an object-oriented program should be, at least by my standards, which prefer more little objects and carefully-divided responsibilities. That’s not to say that I always accomplish that, but it is what I prefer.
But it’s also paying off by helping me learn Python in more depth. Python is sufficiently like Ruby or C or Lua or a host of other languages to allow me to program well enough while knowing only the rudiments of the language. But the more I learn, the more my code will look “pythonic”, and, since the language is designed to support that kind of code, things will go more smoothly.
It’s fun. I like learning, on those rare occasions where it happens. Yesterday and today were two of those occasions.
Do you have Python learning ideas for me? Toot me up. Or tweet, I’m still hanging on in Twitter by one fingernail.
See you next time!