Simpler
Hello, loves!
With respect to Leonard Cohen, I want it simpler, not darker. Let’s see what we can figure out. I am pleased.
Our current effort is to try to write the CombinedContent class, because the abstract class plus five subclasses is clearly not simple enough. For the new object, which so far has no behavior beyond initializing its texture / sprite, I’ve been talking about plugging the necessary behavior into it, thinking that there would be an array of things that need to be done in interact_with_player and another array for run, the only two required methods so far. We’d create the kind of Content we want by appending behaviors to those arrays.
Last evening, I was researching techniques for pluggable behavior in Python, and learned a thing that I didn’t know, about types.MethodType(), which converts a function into a callable thing that expects to receive self as its first parameter. It looks interesting, though a bit deep in the bag of tricks. We might play with that idea sometime, but we’ll set it aside for now.
Another possibility would be the Method Object / Strategy approach, where the constructor of a CombinedContent instance would pass in an object that would have a known calling sequence, presumably the same as the original method, interact or run, perhaps with an additional parameter to talk back to the Content item.
Another possibility would be to have all the possible bits of code that we need already be methods in CombinedContent, and to have some selector scheme that decides which ones to run. Or, ideally, which one to run. Our constructor would pass in methods for interact and run, and __init__ would set a callable interact or run member variable to be that method.
To make a fair decision among these ideas, we’d have to try them all. But no one promised we’d be fair, although I do like to seem fair. And since we’re all about changing code, I think we’ll try one or more, in the order that seems simplest, and when we find something that seems reasonable, go with it until events prove we need more.
The one that seems simplest to me is to have callable instance variables, and predefined methods, and to construct what we want from them. Conveniently we have a test file set up, so let’s see what we find.
Here’s where we stand now:
class TestContentNew:
@property
def resources(self):
return ['abc', 'def']
def test_first(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.75)
assert content.name == 'item'
assert content.resources == self.resources
assert content.scale == 0.75
assert content.interact_with_player(None) == True
class CombinedContent(Content):
def __init__(self, name, resources, scale):
self.name = name
self.resources = resources
self.scale = scale
def run(self, dungeon):
pass
def interact_with_player(self, dungeon) -> bool:
return True
I think that for now, I’ll write additional “setter” methods to assign the desired behavior, though I think we’ll probably want them to be part of a complete creation method before we’re done.
The idea here is that we add methods to the class that are never intended to be called directly, but that the ones assigned to two new instance variables will be called on run and interact. To make the object safe, we’ll begin with the pass methods, and since I’m not sure which of the methods we’ll want to add next, we’ll do both now. This is a refactoring, based on one test, which will suffice.
I just spent two minutes editing the code above in the article. Duh. Better to edit it in the program.
class CombinedContent(Content):
def __init__(self, name, resources, scale):
self.name = name
self.resources = resources
self.scale = scale
self.interact_method = self.interact_true
self.run_method = self.run_pass
def interact_with_player(self, dungeon) -> bool:
return self.interact_method(dungeon)
def interact_true(self, dungeon):
return True
def run(self, dungeon):
self.run_method(dungeon)
def run_pass(self, dungeon):
pass
This runs green. Commit: adding pluggable behavior. (This whole idea may be bereft but we’ll deal with that possibility if it happens. For now, I’d rather have the save points. We can always reset.)
We can do a quick test to make an object that you can’t step on:
def test_cannot_enter(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
content.interact_method = content.interact_false
assert content.interact_with_player(None) == False
Fails for want of interact_false, which we provide:
def interact_false(self, dungeon) -> bool:
return False
Green. Commit.
Now I’d like to be sure that we can use the class to reference the methods, as our constructors may not be allowed to do this setting.
That did not go quite as I anticipated, but I think I understand what’s up. This test passes:
def test_reference_class_for_method(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
unbound = CombinedContent.interact_false
bound = types.MethodType(unbound, content)
content.interact_method = bound
assert content.interact_with_player(None) == False
Without the MethodType incantation, we get an error message saying that the method doesn’t have its dungeon parameter. What has happened is that when we assign a function to an instance variable, Python will not automatically pass self to it when we call it. We could pass it explicitly when we call, but the MethodType wraps the function with the instance we give it, so the function sees the instance as self.
I’m declaring that notion to be too deep in the bag of tricks, at least for now. Perhaps someday I’ll dig into the details of how Python does that stuff.
One possibility would be to have a setter for interact_method that did the wrapping inside, allowing builders to refer to the class to get the method. Let’s try that.
def test_setter_converts_to_bound_method(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
unbound = CombinedContent.interact_false
content.set_interact(unbound)
assert content.interact_with_player(None) == False
This fails for want of the set, which we provide:
def set_interact(self, unbound):
self.interact_method = types.MethodType(unbound, self)
That hides the weirdness inside the class that needs it. What would happen if we sent an already bound method in? It doesn’t work: the double-bound thing expects self twice, once for each layer. No surprise there, I guess.
What are the types here? Could we benefit from checking? I think we can, with a bit of a hack. Here’s a test for passing a method to the set:
def test_setter_does_not_double_wrap_method(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
bound = content.interact_false
content.set_interact(bound)
assert content.interact_with_player(None) == False
It fails because of the double wrapping. But we can do this:
def set_interact(self, func):
if isinstance(func, types.FunctionType):
func = types.MethodType(func, self)
self.interact_method = func
So if it’s a function we wrap it. If not, we’re good to go.
Let’s turn the instance variable into a property, and make the setter work as we see here.
That’s readily done. Let’s reflect. Here are all the tests and the code, working so far:
class TestContentNew:
@property
def resources(self):
return ['abc', 'def']
def test_first(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.75)
assert content.name == 'item'
assert content.resources == self.resources
assert content.scale == 0.75
assert content.interact_with_player(None) == True
def test_cannot_enter(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
content.interact_method = content.interact_false
assert content.interact_with_player(None) == False
def test_reference_class_for_method(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
unbound = CombinedContent.interact_false
bound = types.MethodType(unbound, content)
content.interact_method = bound
assert content.interact_with_player(None) == False
def test_setter_converts_to_bound_method(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
unbound = CombinedContent.interact_false
content.interact_method = unbound
assert content.interact_with_player(None) == False
def test_setter_does_not_double_wrap_method(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
bound = content.interact_false
content.interact_method = bound
assert content.interact_with_player(None) == False
class CombinedContent(Content):
def __init__(self, name, resources, scale):
self.name = name
self.resources = resources
self.scale = scale
self._interact_method = self.interact_true
self.run_method = self.run_pass
@property
def interact_method(self):
return self._interact_method
@interact_method.setter
def interact_method(self, func):
if isinstance(func, types.FunctionType):
func = types.MethodType(func, self)
self._interact_method = func
def interact_with_player(self, dungeon) -> bool:
return self._interact_method(dungeon)
def interact_false(self, dungeon) -> bool:
return False
def interact_true(self, dungeon) -> bool:
return True
def run(self, dungeon):
self.run_method(dungeon)
def run_pass(self, dungeon):
pass
Reflection
I suspect we’re at a stopping point for now. It feels like a nicely closed loop.
Assuming we go forward with this scheme, we’ll provide more and more methods to the class, for each kind of interact_with_player or run behavior that we need. Optionally, we can (probably) put methods in other classes breaking out what we need, or (probably) provide behavior in a lambda. The setters on interact_method and run_method will allow us to be pretty flexible in what we do, although I think we’ll be wise to use essentially none of that possible flexibility, instead just adding methods to CombinedContent, and putting in place the ones we need for the behavior we want.
To decide whether to go forward, I’d propose two steps. First, we’ll review the existing Content subclasses and see whether we can express their behavior readily to CombinedContent. Assuming that that looks good, we’ll blue-sky a few more complex content ideas, to see how the scheme holds up.
My hope is that it’ll hold up. My intuition suspects that it will not, that we’ll want combinations of more than one method for some purpose, though I don’t have one in mind. Be that as it may, if it holds up, we’ll stay on course, and if it doesn’t, we’ll see what it takes to make it capable of what we need.
I am confident now that we can get all the behavior into one class, and while it may have a lot of associated methods, the bulk of them are just sitting there to be plugged into interact or run and are otherwise unused.
Possibly there’ll be a way to represent them in groups or some other scheme so the class doesn’t just become a long unintelligible list of methods.
There is that bit of magic code that wraps a pure function, but I think that might actually turn out to be a good thing. Let me try to write another test.
def test_lambda(self):
content = CombinedContent(name='item', resources=self.resources, scale=0.5)
content.interact_method = lambda self, dungeon: False
assert content.interact_with_player(None) == False
That passes. So we can create behavior anywhere and pass it in. (We’ll want to try creating a method in one class and passing it in here. I don’t know whether that will work or not. I suspect not, unless we pass it unbound. Very weird idea. Danger, Will Robinson, warning warning.)
But overall, I think what we have here is pretty decent. When next we meet, we’ll try configuring more of the exiting content classes into this one. My guess is that it’ll go pretty smoothly.
See you next time!