P-256 - What now?
Python Asteroids+Invaders on GitHub
What shall we do today, Brain? The same thing we do every day, Ron. Find something interesting to do. Things get very meta …
Offline, I made the changes mentioned yesterday, requiring all objects to implement interact_with
, and allowing them to inherit a pass
for interact_with_foo
for any Foo they wish to ignore. Nothing interesting happened, it was all simple changes.
You know what? “Ignore” is an interesting word. For an object to explicitly ignore, say interact_with_bumper
, I have to put three lines into the file:
def interact_with_bumper(self, _bumper, _fleets):
pass
It’s three because PyCharm will complain if I don’t put a blank line between methods. (PEP8 E301)
Because those lines are intricate and all just a bit different, they are distracting. That’s why we allow the developers to put them in the interface but not as abstract methods, so that if an object isn’t interested in bumpers, it just doesn’t override the default pass
.
What if an object could declare the methods that it wanted to ignore? Look at Bumper, which explicitly passes on a lot of things:
class Bumper(InvadersFlyer):
def interact_with_bumper(self, bumper, fleets):
pass
def interact_with_invaderexplosion(self, explosion, fleets):
pass
def interact_with_invaderfleet(self, invader, fleets):
pass
def interact_with_invaderplayer(self, invader, fleets):
pass
def interact_with_invadershot(self, bumper, fleets):
pass
def interact_with_playerexplosion(self, _explosion, _fleets):
pass
def interact_with_shield(self, shield, fleets):
pass
def interact_with_playershot(self, bumper, fleets):
pass
def interact_with_shotcontroller(self, controller, fleets):
pass
def interact_with_shotexplosion(self, bumper, fleets):
pass
def interact_with_topbumper(self, top_bumper, fleets):
pass
Isn’t that just awful?? Suppose that instead it could have said something like this:
def __init__(...)
...
self.ignore(
"interact_with_bumper".
"interact_with_invaderexplosion".
"interact_with_invaderfleet".
"interact_with_invaderplayer".
"interact_with_invadershot".
"interact_with_playerexplosion".
"interact_with_shield".
"interact_with_playershot".
"interact_with_shotcontroller".
"interact_with_shotexplosion".
"interact_with_topbumper"
)
That might be a lot better. It’s more declarative, more compact, and doesn’t force us to search through as much cruft when we’re looking for something.
I have questions.
How could this work? Would we monkey-patch the instance, adding those methods as pass
? Nasty, but maybe worth it?
Could we do it safely? What should happen if you say you ignore something and then later in the file def that thing? Should it still get hammered by the ignore? Could we detect errors, so that if you said imperaft_wif_bunker
you’d get an error suggesting a spell checker?
I propose to test this idea. Now we know what we’re going to do today!
Ignore, via Monkey Patching
class MonkeyVictim:
def nothing(self):
pass
def three(self):
return 3
def four(self):
return 4
def test_simple_monkey(self):
m = MonkeyVictim()
assert m.four() == 4
setattr(m, "four", m.nothing)
assert not m.four()
The test runs. After the setattr
, m
no longer returns 4 from a call to four
. Let’s make sure we only affected this instance. It’s clear that we did but let’s be sure.
- Note
- Full disclosure, I started this using
__setattr__
, which works, but vanillasetattr
seems to be the approved way of doing this. I’ve revised what follows to make it seem that I just did that from the beginning, but I want you to know that I did make that decision and reverse it.
def test_monkey_independent(self):
m1 = MonkeyVictim()
m2 = MonkeyVictim()
setattr(m1, "four", m1.nothing)
assert not m1.four()
assert m2.four() == 4
Now let’s see if we can drive out a method of some kind. For now I’ll put it in MonkeyVictim, but I am wondering how we’ll really wire it in.
class MonkeyVictim:
def ignore(self, name):
setattr(self, name, self.nothing)
I added that before testing. Forgive me.
def test_ignore_method(self):
m = MonkeyVictim()
assert m.four() == 4
m.ignore("four")
assert not m.four()
Now what I want is a safe ignore, one that will not override a method that exists:
def test_safe_ignore(self):
m = MonkeyVictim()
assert m.four() == 4
m.safe_ignore("four")
assert m.four() == 4
That should be easy:
class MonkeyVictim:
def safe_ignore(self,name):
if not getattr(self, name):
self.ignore(name)
Reflection
Well, that’s all it really takes to do the patching. We have a valid spike of monkey patching to pass
. Commit: TDD some monkey patching.
I remain uncertain as to the best way to actually provide this feature. And we have more to do. What we have done here is to patch an instance. We’d really like this declaration to patch the class itself. We’re essentially auto-implementing certain methods, and we want them to be that way for all instances. So there’s more learning to do
Back to it
Let’s do a list while we’re here. Just kind of flexing a bit, getting the feeling for all this.
def test_safe_list(self):
m = MonkeyVictim()
m.ignore_list("four", "six")
assert m.four() == 4
assert not m.six()
Here I expect four
to be preserved and six
to be created as a pass. My attempt fails:
class MonkeyVictim:
def ignore_list(self, *names):
for name in names:
self.safe_ignore(name)
def safe_ignore(self,name):
if not getattr(self, name):
self.ignore(name)
def ignore(self, name):
setattr(self, name, self.nothing)
The getattr
throws if it does not find the name. Clearly I have not written a test for that side of safe_ignore
. Nice catch.
We can do this:
def safe_ignore(self,name):
try:
getattr(self, name)
except AttributeError:
self.ignore(name)
Tests are green. The pythonic way, I’ve read, is to try things and field the exception rather than check and then do. I hope that’s true. In any case we are green. Commit: ignore-list works.
Reflection
It’s getting pretty dark in here: we’re rather deep in the bag of tricks. That’s OK, we’re learning some details about Python. We may or may not do this in our production code, depending on what we learn.
I think we want to explore how to build a class all of whose instances safely ignore a list of methods that are not defined in the class, while not ignoring an item in the list if it occurs subsequently in the class. It’s possible that if we can really do it at the class definition level, the subsequent defines will just override any pass
methods that we smush in the class definition.
For that to work, I think we would need a Python metaclass. We want to say something like this:
class Bumper(InvadersFlyer, metaclass=Ignorables, ignores=[
"interact_with_bumper".
"interact_with_invaderexplosion".
"interact_with_invaderfleet".
"interact_with_invaderplayer".
"interact_with_invadershot".
"interact_with_playerexplosion".
"interact_with_shield".
"interact_with_playershot".
"interact_with_shotcontroller".
"interact_with_shotexplosion".
"interact_with_topbumper"
]
)
(I freely grant that I have no idea how to implement that, but I think it’s a pretty good guess at the syntax. We could of course do something like this for neatness:)
_ignores = [
"interact_with_bumper".
"interact_with_invaderexplosion".
"interact_with_invaderfleet".
"interact_with_invaderplayer".
"interact_with_invadershot".
"interact_with_playerexplosion".
"interact_with_shield".
"interact_with_playershot".
"interact_with_shotcontroller".
"interact_with_shotexplosion".
"interact_with_topbumper"
]
class Bumper(InvadersFlyer, metaclass=Ignorables, ignores=_ignores)
Before I’m ready for that, I want to do some more reading. I think this would be a rather decent way to safely declare the calls that an object ignores without too much textual noise. It will surely be a bit deep in the bag of tricks, but I’d like to at least see it before I reject it. Besides, learning.
I’ll be back, soon, either to try this or declare defeat. Most likely the former. I can always be defeated later.
See you then!