After reading Ethan's email, I decided to play more with the vec2d class I posted, specifically to try to understand how bad the allocations due to operator overloading really are - in that I want to put it in a real context (like say how many particle positions you could translate and still get 60 fps)... cause it doesn't matter if you make the thing that takes 1% of your time go 1000% faster...
so I wrote test functions to take a 2d position, subtract another position from it then multiply the result by a scalar (basically pan and zoom a position). One version uses the 2d vector with operator overloading, so it will allocate 2 vec2d's, each with a list member. Another version does explicit math operations on variables, so no allocations happen. I also tried converting the vec2d class to be inherited from tuple, thinking that maybe you could save the list member allocations by being a tuple to start with... Then for performance, I ran the script in Python 2.3 on my 800Mhz p3 laptop plugged into the wall (what I consider my min spec) then checked how many times it could do the translations a second. Basically, using the operator overloading cut the performance by about a factor of 18 . Also, when running psycho, the func doing math on variables was able to run about 3x faster, but the operator overloading routines weren't able to go significantly faster (makes sense cause psycho doesn't help with allocation issues) making it more like a 48:1 performance diff. Also the tuple inherited version was slower. So then using the loops/sec to try and figure out some context for the perf difference, I computed how many times you could run the loop if you had a 5% budget of cpu time for a game that ran at 60fps on my min-spec machine - and that figure was 15/280 without psycho, or 16/760 with it. only moving around 15 objects a frame on my laptop before dropping frames does strike me as very bad... especially when I'd probably be pretty happy with 280 objects... but I'll still probably keep using vec2d as a rule (although I do know where to look first when performance drags now...) I'm attaching the adapted code, if anyone is curious (it will run the perf tests and print out stats)
""" 2D Vector Math Class (with operator overload goodness) (C) Copyright 2005 James Paige and Hamster Republic Productions """ ######################################################################## import operator import math ######################################################################## class vec2d(object): def __init__(self, x_or_pair, y = None): if y == None: try: self.vec = [x_or_pair[0],x_or_pair[1]] except TypeError: raise TypeError("vec2d constructor requires a tuple or two arguments") else: self.vec = [x_or_pair,y] def get_x(self): return self.vec[0] def set_x(self, value): self.vec[0] = value x = property(get_x, set_x) def get_y(self): return self.vec[1] def set_y(self, value): self.vec[1] = value y = property(get_y, set_y) def set(self, x, y): self.vec[0] = x self.vec[1] = y # String representaion (for debugging) def __repr__(self): return 'vec2d(%s, %s)' % (self.x, self.y) # Array-style access def __len__(self): return 2 def __getitem__(self, key): return self.vec[key] def __setitem__(self, key, value): self.vec[key] = value # Comparison def __eq__(self, other): return self.vec[0] == other[0] and self.vec[1] == other[1] def __ne__(self, other): return self.vec[0] != other[0] or self.vec[1] != other[1] def __nonzero__(self): return self.vec[0] or self.vec[1] # Generic operator handlers def _o2(self, other, f): "Any two-operator operation where the left operand is a vec2d" try: return vec2d(f(self.vec[0], other[0]), f(self.vec[1], other[1])) except TypeError: return vec2d(f(self.vec[0], other), f(self.vec[1], other)) def _r_o2(self, other, f): "Any two-operator operation where the right operand is a vec2d" try: return vec2d(f(other[0], self.vec[0]), f(other[1], self.vec[1])) except TypeError: return vec2d(f(other, self.vec[0]), f(other, self.vec[1])) def _o1(self, f): "Any unary operation on a vec2d" return vec2d(f(self.vec[0]), f(self.vec[1])) # Addition def __add__(self, other): return self._o2(other, operator.add) __radd__ = __add__ # Subtraction def __sub__(self, other): return self._o2(other, operator.sub) def __rsub__(self, other): return self._r_o2(other, operator.sub) # Multiplication def __mul__(self, other): return self._o2(other, operator.mul) __rmul__ = __mul__ # Division def __div__(self, other): return self._o2(other, operator.div) def __rdiv__(self, other): return self._r_o2(other, operator.div) def __floordiv__(self, other): return self._o2(other, operator.floordiv) def __rfloordiv__(self, other): return self._r_o2(other, operator.floordiv) def __truediv__(self, other): return self._o2(other, operator.truediv) def __rtruediv__(self, other): return self._r_o2(other, operator.truediv) # Modulo def __mod__(self, other): return self._o2(other, operator.mod) def __rmod__(self, other): return self._r_o2(other, operator.mod) def __divmod__(self, other): return self._o2(other, operator.divmod) def __rdivmod__(self, other): return self._r_o2(other, operator.divmod) # Exponentation def __pow__(self, other): return self._o2(other, operator.pow) def __rpow__(self, other): return self._r_o2(other, operator.pow) # Bitwise operators def __lshift__(self, other): return self._o2(other, operator.lshift) def __rlshift__(self, other): return self._r_o2(other, operator.lshift) def __rshift__(self, other): return self._o2(other, operator.rshift) def __rrshift__(self, other): return self._r_o2(other, operator.rshift) def __and__(self, other): return self._o2(other, operator.and_) __rand__ = __and__ def __or__(self, other): return self._o2(other, operator.or_) __ror__ = __or__ def __xor__(self, other): return self._o2(other, operator.xor) __rxor__ = __xor__ # Unary operations def __neg__(self): return self._o1(operator.neg) def __pos__(self): return self._o1(operator.pos) def __abs__(self): return self._o1(operator.abs) def __invert__(self): return self._o1(operator.invert) # vectory functions def get_length_sqrd(self): return self.vec[0]**2 + self.vec[1]**2 def get_length(self): return math.sqrt(self.vec[0]**2 + self.vec[1]**2) def __setlength(self, value): self.normalize_return_length() self.vec[0] *= value self.vec[1] *= value length = property(get_length, __setlength, None, "gets or sets the magnitude of the vector") def rotate(self, angle_degrees): radians = math.radians(angle_degrees) cos = math.cos(radians) sin = math.sin(radians) x = self.vec[0]*cos - self.vec[1]*sin y = self.vec[0]*sin + self.vec[1]*cos self.vec[0] = x self.vec[1] = y def get_angle(self): if (self.get_length_sqrd() == 0): return 0 return math.degrees(math.atan2(self.vec[1], self.vec[0])) def get_angle_between(self, other): cross = self.vec[0]*other[1] - self.vec[1]*other[0] dot = self.vec[0]*other[0] + self.vec[1]*other[1] return math.degrees(math.atan2(cross, dot)) def __setangle(self, angle_degrees): self.vec[0] = self.length self.vec[1] = 0 self.rotate(angle_degrees) angle = property(get_angle, __setangle, None, "gets or sets the angle of a vector") def normalized(self): length = self.length if length != 0: return self/length return vec2d(self) def perpendicular(self): return vec2d(-self.vec[1], self.vec[0]) def perpendicular_normal(self): length = self.length if length != 0: return vec2d(-self.vec[1]/length, self.vec[0]/length) return vec2d(self) def normalize_return_length(self): length = self.length if length != 0: self.vec[0] /= length self.vec[1] /= length return length def dot(self, other): return self.vec[0]*other[0] + self.vec[1]*other[1] def get_distance(self, other): return math.sqrt((self.vec[0] - other[0])**2 + (self.vec[1] - other[1])**2) def projection(self, other): normal = other.normalized() projected_length = self.dot(normal) return normal*projected_length def cross(self, other): return self.vec[0]*other[1] - self.vec[1]*other[0] def interpolate_to(self, other, range): return vec2d(self.vec[0] + (other.vec[0] - self.vec[0])*range, self.vec[1] + (other.vec[1] - self.vec[1])*range) def convert_to_basis(self, x_vector, y_vector): return vec2d(self.dot(x_vector)/x_vector.get_length_sqrd(), self.dot(y_vector)/y_vector.get_length_sqrd()) ######################################################################## class vec2dTuple(tuple): def __new__(cls, *args): if len(args) == 1: return tuple.__new__(cls, args[0]) else: return tuple.__new__(cls, args) def get_x(self): return self[0] x = property(get_x) def get_y(self): return self[1] y = property(get_y) # String representaion (for debugging) def __repr__(self): return 'vec2dTuple(%s, %s)' % (self.x, self.y) # Comparison def __eq__(self, other): return self[0] == other[0] and self[1] == other[1] def __ne__(self, other): return self[0] != other[0] or self[1] != other[1] def __nonzero__(self): return self[0] or self[1] # Generic operator handlers def _o2(self, other, f): "Any two-operator operation where the left operand is a vec2dTuple" try: return vec2dTuple(f(self[0], other[0]), f(self[1], other[1])) except TypeError: return vec2dTuple(f(self[0], other), f(self[1], other)) def _r_o2(self, other, f): "Any two-operator operation where the right operand is a vec2dTuple" try: return vec2dTuple(f(other[0], self[0]), f(other[1], self[1])) except TypeError: return vec2dTuple(f(other, self[0]), f(other, self[1])) def _o1(self, f): "Any unary operation on a vec2dTuple" return vec2dTuple(f(self[0]), f(self[1])) # Addition def __add__(self, other): return self._o2(other, operator.add) __radd__ = __add__ # Subtraction def __sub__(self, other): return self._o2(other, operator.sub) def __rsub__(self, other): return self._r_o2(other, operator.sub) # Multiplication def __mul__(self, other): return self._o2(other, operator.mul) __rmul__ = __mul__ # Division def __div__(self, other): return self._o2(other, operator.div) def __rdiv__(self, other): return self._r_o2(other, operator.div) def __floordiv__(self, other): return self._o2(other, operator.floordiv) def __rfloordiv__(self, other): return self._r_o2(other, operator.floordiv) def __truediv__(self, other): return self._o2(other, operator.truediv) def __rtruediv__(self, other): return self._r_o2(other, operator.truediv) # Modulo def __mod__(self, other): return self._o2(other, operator.mod) def __rmod__(self, other): return self._r_o2(other, operator.mod) def __divmod__(self, other): return self._o2(other, operator.divmod) def __rdivmod__(self, other): return self._r_o2(other, operator.divmod) # Exponentation def __pow__(self, other): return self._o2(other, operator.pow) def __rpow__(self, other): return self._r_o2(other, operator.pow) # Bitwise operators def __lshift__(self, other): return self._o2(other, operator.lshift) def __rlshift__(self, other): return self._r_o2(other, operator.lshift) def __rshift__(self, other): return self._o2(other, operator.rshift) def __rrshift__(self, other): return self._r_o2(other, operator.rshift) def __and__(self, other): return self._o2(other, operator.and_) __rand__ = __and__ def __or__(self, other): return self._o2(other, operator.or_) __ror__ = __or__ def __xor__(self, other): return self._o2(other, operator.xor) __rxor__ = __xor__ # Unary operations def __neg__(self): return self._o1(operator.neg) def __pos__(self): return self._o1(operator.pos) def __abs__(self): return self._o1(operator.abs) def __invert__(self): return self._o1(operator.invert) # vectory functions def get_length_sqrd(self): return self[0]**2 + self[1]**2 def get_length(self): return math.sqrt(self[0]**2 + self[1]**2) length = property(get_length, None, None, "gets or sets the magnitude of the vector") def get_angle(self): if (self.get_length_sqrd() == 0): return 0 return math.degrees(math.atan2(self[1], self[0])) def get_angle_between(self, other): cross = self[0]*other[1] - self[1]*other[0] dot = self[0]*other[0] + self[1]*other[1] return math.degrees(math.atan2(cross, dot)) angle = property(get_angle, None, None, "gets or sets the angle of a vector") def normalized(self): length = self.length if length != 0: return self/length return vec2dTuple(self) def perpendicular(self): return vec2dTuple(-self[1], self[0]) def perpendicular_normal(self): length = self.length if length != 0: return vec2dTuple(-self[1]/length, self[0]/length) return vec2dTuple(self) def normalize_return_length(self): length = self.length if length != 0: self[0] /= length self[1] /= length return length def dot(self, other): return self[0]*other[0] + self[1]*other[1] def get_distance(self, other): return math.sqrt((self[0] - other[0])**2 + (self[1] - other[1])**2) def projection(self, other): normal = other.normalized() projected_length = self.dot(normal) return normal*projected_length def cross(self, other): return self[0]*other[1] - self[1]*other[0] def interpolate_to(self, other, range): return vec2dTuple(self[0] + (other[0] - self[0])*range, self[1] + (other[1] - self[1])*range) def convert_to_basis(self, x_vector, y_vector): return vec2dTuple(self.dot(x_vector)/x_vector.get_length_sqrd(), self.dot(y_vector)/y_vector.get_length_sqrd()) ######################################################################## ## Unit Testing ## ######################################################################## if __name__ == "__main__": import unittest #################################################################### class UnitTestVec2D(unittest.TestCase): def setUp(self): pass def testCreationAndAccess(self): v = vec2d(111,222) self.assert_(v.x == 111 and v.y == 222) v.x = 333 v[1] = 444 self.assert_(v[0] == 333 and v[1] == 444) def testMath(self): v = vec2d(111,222) self.assert_(v + 1 == vec2d(112,223)) self.assert_(v - 2 == [109,220]) self.assert_(v * 3 == (333,666)) self.assert_(v / 2.0 == vec2d(55.5, 111)) self.assert_(v / 2 == (55, 111)) self.assert_(v ** vec2d(2,3) == [12321, 10941048]) self.assert_(v + [-11, 78] == vec2d(100, 300)) self.assert_(v / [11,2] == [10,111]) def testReverseMath(self): v = vec2d(111,222) self.assert_(1 + v == vec2d(112,223)) self.assert_(2 - v == [-109,-220]) self.assert_(3 * v == (333,666)) self.assert_([222,999] / v == [2,4]) self.assert_([111,222] ** vec2d(2,3) == [12321, 10941048]) self.assert_([-11, 78] + v == vec2d(100, 300)) def testUnary(self): v = vec2d(111,222) v = -v self.assert_(v == [-111,-222]) v = abs(v) self.assert_(v == [111,222]) def testLength(self): v = vec2d(3,4) self.assert_(v.length == 5) self.assert_(v.get_length_sqrd() == 25) self.assert_(v.normalize_return_length() == 5) self.assert_(v.length == 1) v.length = 5 self.assert_(v == vec2d(3,4)) v2 = vec2d(10, -2) self.assert_(v.get_distance(v2) == (v - v2).get_length()) def testAngles(self): v = vec2d(0, 3) self.assertEquals(v.angle, 90) v2 = vec2d(v) v.rotate(-90) self.assertEqual(v.get_angle_between(v2), 90) v2.angle -= 90 self.assertEqual(v.length, v2.length) self.assertEquals(v2.angle, 0) self.assertEqual(v2, [3, 0]) self.assert_((v - v2).length < .00001) self.assertEqual(v.length, v2.length) v2.rotate(300) self.assertAlmostEquals(v.get_angle_between(v2), -60) v2.rotate(v2.get_angle_between(v)) angle = v.get_angle_between(v2) self.assertAlmostEquals(v.get_angle_between(v2), 0) def testHighLevel(self): basis0 = vec2d(5.0, 0) basis1 = vec2d(0, .5) v = vec2d(10, 1) self.assert_(v.convert_to_basis(basis0, basis1) == [2, 2]) self.assert_(v.projection(basis0) == (10, 0)) self.assert_(basis0.dot(basis1) == 0) def testCross(self): lhs = vec2d(1, .5) rhs = vec2d(4,6) self.assert_(lhs.cross(rhs) == 4) #################################################################### class UnitTestvec2dTuple(unittest.TestCase): def setUp(self): pass def testCreationAndAccess(self): v = vec2dTuple(111,222) self.assert_(v.x == 111 and v.y == 222) def testMath(self): v = vec2dTuple(111,222) self.assert_(v + 1 == vec2dTuple(112,223)) self.assert_(v - 2 == [109,220]) self.assert_(v * 3 == (333,666)) self.assert_(v / 2.0 == vec2dTuple(55.5, 111)) self.assert_(v / 2 == (55, 111)) self.assert_(v ** vec2dTuple(2,3) == [12321, 10941048]) self.assert_(v + [-11, 78] == vec2dTuple(100, 300)) self.assert_(v / [11,2] == [10,111]) self.assert_(v + (89, -122) == [200,100]) def testReverseMath(self): v = vec2dTuple(111,222) self.assert_(1 + v == vec2dTuple(112,223)) self.assert_(2 - v == [-109,-220]) self.assert_(3 * v == (333,666)) self.assert_([222,999] / v == [2,4]) self.assert_([111,222] ** vec2dTuple(2,3) == [12321, 10941048]) self.assert_([-11, 78] + v == vec2dTuple(100, 300)) self.assert_((89,-122) + v == [200,100]) def testUnary(self): v = vec2dTuple(111,222) v = -v self.assert_(v == [-111,-222]) v = abs(v) self.assert_(v == [111,222]) def testLength(self): v = vec2dTuple(3,4) self.assert_(v.length == 5) self.assert_(v.get_length_sqrd() == 25) v2 = v.normalized() self.assert_(v2.length == 1) v2 = vec2dTuple(10, -2) self.assert_(v.get_distance(v2) == (v - v2).get_length()) def testAngles(self): v = vec2dTuple(0, 3) self.assertEquals(v.angle, 90) v2 = vec2dTuple(v) self.assertEqual(v.length, v2.length) def testHighLevel(self): basis0 = vec2dTuple(5.0, 0) basis1 = vec2dTuple(0, .5) v = vec2dTuple(10, 1) self.assert_(v.convert_to_basis(basis0, basis1) == [2, 2]) self.assert_(v.projection(basis0) == (10, 0)) self.assert_(basis0.dot(basis1) == 0) def testCross(self): lhs = vec2dTuple(1, .5) rhs = vec2dTuple(4,6) self.assert_(lhs.cross(rhs) == 4) ######################################################################## import time def perfTest(func, num_runs = 10000, fps=60, budget=.05): start = time.clock() func(num_runs) end = time.clock() elapsed = end - start print "%s runs at %.1f loops/s" % (str(func), num_runs/elapsed) print "that's %.2f loops to fill %d fps" % (num_runs/elapsed/fps, fps) print "or %.2f at a %.1f%% budget at %d fps" % (budget*num_runs/elapsed/fps, budget*100.0, fps) class TestData: screen_offsetx = 100 screen_offsety = 120 screen_offset = vec2d(screen_offsetx, screen_offsety) screen_offset_tuple = vec2dTuple(screen_offsetx, screen_offsety) screen_scale = .5 object_positionx = 130.5 object_positiony = 191.5 object_position = vec2d(object_positionx, object_positiony) object_position_tuple = vec2dTuple(object_positionx, object_positiony) def ScreenTranslationTestVec2d(loop_count): for i in xrange(loop_count): final_pos = (TestData.object_position_tuple - TestData.screen_offset_tuple)*TestData.screen_scale def ScreenTranslationTestVec2dTuple(loop_count): for i in xrange(loop_count): final_pos = (TestData.object_position_tuple - TestData.screen_offset_tuple)*TestData.screen_scale def ScreenTranslationTestExplicit(loop_count): for i in xrange(loop_count): final_posx = (TestData.object_positionx - TestData.screen_offsetx)*TestData.screen_scale final_posy = (TestData.object_positiony - TestData.screen_offsety)*TestData.screen_scale ######################################################################## print "testing vector code..." perfTest(ScreenTranslationTestVec2d) perfTest(ScreenTranslationTestVec2dTuple) perfTest(ScreenTranslationTestExplicit) print "\nnow psychoing everything..." import psyco psyco.bind(ScreenTranslationTestVec2d) psyco.bind(ScreenTranslationTestVec2dTuple) psyco.bind(ScreenTranslationTestExplicit) perfTest(ScreenTranslationTestVec2d) perfTest(ScreenTranslationTestVec2dTuple) perfTest(ScreenTranslationTestExplicit) ##################################################################### unittest.main() #####################################################################