P-286: More Expressive
Python Asteroids+Invaders on GitHub
I have a somewhat more expressive way to, well, express what we did yesterday. Do we prefer it? Let’s find out. Short, and sweet.
Yesterday, in PlayerMaker, we set some member variables to (the name of) a method, and then elsewhere in the code, called that member just like a method. This works exactly as advertised because all the methods in Python are just member variables that are callable. But when we read the code, it can be confusing:
def interact_with_invaderplayer(self, _player, _fleets):
self.pluggable_final_action = self.final_do_nothing
def end_interactions(self, fleets):
self.pluggable_final_action(fleets)
As a final act yesterday, I renamed the members to pluggable_final_action
and pluggable_reserve_action
, to try to make it a bit more clear what’s going on. But I believe that when browsing that code, when we hit end_interactions
, we will automatically scan the methods looking for def pluggable_final_action
. The search will fail and we will have to reload our instruction pipeline to figure out what is going on.
I have an idea for something that might help, and it goes like this:
def end_interactions(self, fleets):
self.perform(self.pluggable_final_action, fleets)
If we could say that, it seems to me that it would be more clear that pluggable_final_action
is in fact a member variable. And if we implement this:
def perform(self, callable_function, arg):
return callable_function(arg)
I think we have every possibility that the reader will not have to flush so much of their instruction pipeline to see what’s going on. I change the other similar method:
def final_deal_with_missing_player(self, fleets):
self.perform(self.pluggable_reserve_action, fleets)
And commit: Use perform to clarify we’re calling a pluggable function.
Let’s reflect.
Reflection
The additional method perform
puts one more level of symbol lookup into the execution of those methods. Python now has to look up perform
and then look up pluggable_whatever
, where before it just had to do the latter. So it is less efficient. If this code ran every microsecond, I might be concerned. as it stands, it runs only 60 times a second, and I am not concerned.
I wonder how long it takes to look up a member. Let’s find out.
def test_lookup(self):
class Something:
def __init__(self):
self.value = 0
something = Something()
n = 1000000
t0 = time.time()
for i in range(n):
pass
t1 = time.time()
for i in range(n):
something.value
t2 = time.time()
for i in range(n):
x = something.value
t3 = time.time()
easy = t1 - t0
medium = t2 - t1
hard = t3 - t2
print(easy, medium, hard)
assert False
Results:
0.010064125061035156 0.013721942901611328 0.014167070388793945
So a million lookups required 0.004 seconds, or 0.000 000 004 seconds each. Counting on my fingers, that suggests that each lookup requires 4 nanoseconds. On my M1 MacBook Air, that’s about 13 instruction times. Does the math check out? I’m not sure but I am sure that I can afford to say perform
in those places. I am sure that it is a lot faster than my Apple ][, whose 6502 cycle time was one microsecond. Wow. My laptop is 3000 times faster than the Apple ][? Nice.
Is it a bit more clear what’s going on with the perform
added in there? I believe that it is, and that it will help the reader avoid a cognitive pipeline stall. And that’s a good thing.
There are alternatives. We could use delegation, with one or two little objects to handle the situation. We’ve done that elsewhere to some decent effect. When we use delegation, we get a similar kind of indirection that makes it more clear what’s going on. It might look something like this:
def end_interactions(self, fleets):
self.delegate.execute(fleets)
As my brother Hill likes to put it, the code works for me, I don’t work for the code. So the code needs to be the way I want it, and by and large I want it clean, clear, and well tested. This little change goes toward clean and clear.
Summary
A tiny change like this one makes a small but noticeable improvement in our ability to quickly apprehend the meaning of the code. Every time we make such a change, we’re smoothing our future path just a bit. Is it always “worth it” to do these things?
Well, I wouldn’t necessarily prowl through the code looking for opportunities like this, unless the DELETEDs give us a free coding day or something, but when I run across an opportunity, my inclination would be to take it. It makes me feel better in the moment, and some of those changes are going to help me out in the future.
For me, it’s the former benefit that counts. If I can do my daily work in a way that feeds me little bites of joy, I’ll be more effective and end the day just a bit less likely to drive like an angry screaming madman on the way home. And that’s a good thing.
See you next time!