Speak and Shout

Monday, December 25, 2006

Merry Christmas

And the angel said to them, "Fear not, for behold, I bring you good news of a great joy that will be for all the people. For unto you is born this day in the city of David a Savior, who is Christ the Lord." - Luke 2:10-11 (ESV)

Hope you have a wonderful Christmas.

Tuesday, December 12, 2006

Abstract methods in Python

Following up one of my previous posts, I've been looking at code from Pygame Extended to get ideas for a Scene class to use with my Asteroids game. I'm requiring derived classes to override a process_events function in my Scene class, since each scene will need its own event loop.

In this case, it thought it would be good to have a way to specify that process_events was an abstract method. I guess I hadn't thought about abstract methods too much before in Python since I don't typically use inheritance, but I knew there wasn't anything like the notion of interfaces built into the language itself.

I started looking through the Python Cookbook for ideas and came across this article. Yikes! I wasn't sure I wanted to bother with metaclasses just for this feature. But in the comments I saw that someone else suggested raising an exception in the base class so that calling the method at runtime would fail unless there was an override.

I slapped my forehead after reading the Cookbook comment. Of course, in hindsight, it was obvious. But apparently it isn't obvious to other folks either. The pygext source code simply uses empty methods like the following:

class Foo(object):
   def update(self):
      pass # Override me in derived class


Of course, you can't distinguish pure virtual methods from regular virtual methods this way.

Even some very respectable folks have solved this problem differently: Peter Norvig, when writing his Python code for his Artificial Intelligence book, uses a made-up keyword called "abstract" which fails at runtime if it hasn't been overridden in a derived class. Not the most elegant solution either.

I really think handling abstract methods should be done at object creation like the previously mentioned Cookbook recipe does, but it should be built into Python itself. This may be one of the reasons people think OOP in Python is a hack ...

Labels: , , ,

Friday, December 01, 2006

Nifty Python idiom?

While writing my game, I've been using Python lists to keep track of things on the screen like laser shots and asteroids. On-screen objects typically have limited lifetimes and then get deleted from their associated list. This leads to code that looks something like the following.

import random

class Foo(object):
   def __init__(self):
      self.time = random.randint(1, 100)

   def update(self):
      self.time -= 1

lst = [Foo() for x in range(5)]
while lst:
   delete_lst = []
   for i, x in enumerate(lst):
      x.update()
      if x.time == 0:
         delete_lst.append(i)
   for i in reversed(delete_lst):
      del lst[i]
print "Done!"

To summarize what's going on, a list of 5 Foo objects gets created, each with random time values, and added to a list. After that, a loop is started that calls .update() on each Foo that decrements its .time variable by 1. Once the time value reaches zero, then the object is deleted from the list.

This is clumsy. I can't delete items while I'm iterating through the list, or I'll invalidate the iterator. So I have to keep track of the index of which items need to be deleted, and then actually delete them in a second loop. Also, I have to delete the items in reverse order, or I'll throw off the indexes and delete the wrong items.

I've written code like this several times when working with vectors in C++. So it's familiar, but writing it in Python makes me almost wince.

I felt sure that I should be able to improve this code with a list comprehension, but until this morning, I couldn't see how. But sometimes the drive to work gives me some clarity.

Here's the improved list comprehension version:
import random
class Foo(object):
   def __init__(self):
      self.time = random.randint(1, 100)

   def update(self):
      self.time -= 1
      return self.time > 0

lst = [Foo() for x in range(5)]
while lst:
   lst = [x for x in lst if x.update()]
print "Done!"

Whew - that's so much nicer. The list comprehension updates all items in the list every time through the loop while automatically filtering out the ones that have a time value of zero.

There is the disadvantage of constantly generating additional lists each time through the loop, but since I don't have many objects to manage, I think the code reduction (and resulting clarity) outweighs the performance hit in this scenario.

Of course, other Python gurus probably already know about this trick, but I felt proud of myself for figuring it out.

UPDATE -- It would be nice to post this idiom to the Python Cookbook, but I'm a little baffled as to what to call it. Suggestions?

Labels: , ,