Wednesday 11 February 2009

pyton: everything is an object

Remember that python game framework I was creating? The one with the silly name? (I won't actually tell you what the silly name was, dig through the archives or something). Well, I don't have access to the original code, since it is all in a nice git repository on my pc at home. But this has not prevented me from working on the code.

I've started a new fresh repository on my system here, and to make sure I won't make the same mistake, I've set up a github repository. The new name for the framework is gunge, but the only part that is currently available is the event handling part. I have, however, come across an interesting design challenge that really made me appreciate the fact that in python, everything is an object.

You see, the event system provides a decorator called bind that allows you to statically bind events. It would be used something like so:

class SomeClass(gunge.event.Handler):
@gunge.event.bind(pygame.KEYDOWN, {'unicode': 'a'})
def on_keya(self, event):
print "some code"

The function on_keya would then be called whenever a KEYDOWN event occurs, and furthermore, only if event.unicode is equal to 'a.' There are a few more powerful features to this second argument, called the attribute filter, but that is for another time. How would this be implemented? there is a difference between this function and an actual instance method, bound to an instance.

My first idea was to store the information in some class variable called handlers, and have each instance use this variable to bind its actual methods upon initialization. This works in the simple cases, but becomes problematic with inheritance. A class variable does not carry over nicely between inherited classes, and furthermore, there is the problem of overidden functions. If a function is bound to a certain event in the parent class, and that function is overidden in the child, what should happen? should the parent binding still count, and how would this be implemented?

Implementation issues aside, this method also requires the user to create a new class variable in each class that is an event handler, and pass this handler into the bind decorator so that it can be used for annotation. The problem seemed insolvable. But then a wise lesson came to me: If you're design runs into implementation issues, do not try to solve the implementation issues. It is likely that you need a different design.

It came to me that functions, like pretty much everything in python, are just objects. With types, attributes, the whole shenanigans. So, it seemed much simpler to simply store the binding information as an attribute of the function. Since introspection can be used to find all of an instances methods, it is trivial to retrieve this information. This means that the boiler plate code of the class variable is gone, and that derived classes will retain the bindings of their parent class, unless that particular function is overridden. In that case, the parent functions is not part of the child class, and the binding must be respecified. And this is actually desirable, for clarity's sake.

Furthermore, we can allow a single event Binder object to allow multiple callbacks. This means that a class method can simply store its own Binder object, and each new class instance can simply add its own callback to this object. This reduces the amount of event Binder objects in the event manager drastically, as a class with one method bound to an event will have one Binder object, no matter how many instances of that class exist. This can have huge benefits in both space and speed, since an event has to be tested against an attribute filter just once.

The 'everything is an object' paradigm, together with powerful introspection capabilities, allow you to do a lot more with functions than would normally be possible. And the lesson for this week, of course: If your design has problems, don't try to work around them. Instead, change the design.

No comments: