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?
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?

3 Comments:
There are a couple of interesting things there that I think folks can learn from. As to what to call it... not sure. "looping list filtering with list comprehensions".
I do find myself using list comprehensions and generators much more often now.
By
ScW, At
2:06 PM
If you want to avoid the overhead of unnecessarily recreating the list, here's a much cleaner version than your first approach.
lst = [Foo() for x in range(5)]
while lst:
dropped_any = False
for i,x in enumerate(lst):
if not x.update():
dropped_any = True
lst[i] = None
if dropped_any:
lst = [x for x in lst if x]
print "Done!"
By
AndyDent, At
12:03 AM
Hi Andy --
Your version certainly avoids recreating the list each time, but I certainly don't think replacing 3 lines of code with 9 is cleaner. For some reason, efficiency is the enemy of brevity (and clarity) with this particular task.
Brandon
By
Brandon Corfman, At
8:00 AM
Post a Comment
<$I18N$LinksToThisPost>:
Create a Link
<< Home