Would it slow down the code at all? The module has been consistently tested for speed, and while it may be convenient and simpler (and maybe even proper) to inherit from other classes or else embed an object inside another object, that approach has been rejected in this recipe for optimum speed in pure Python code. Going against that technique may make the code harder to maintain in the long run, but once the classes are properly written, they should require no changes in the future since vector math does not vary from time to time.
Can you provide a short sample implementation using "collections.NamedTuple" that is as fast as the one presented?
Using short names like _s, _c doesn't help speed and is a lot harder to read so I'd stick with the full names sin, cos, etc.
Recipe 277940 would make things faster, and it looks you don't rely on reasignable globals so using it won't hurt.
Inside Polar2, aren't the axes inverted? In usual coordinates, x=Magnitude*cos(angle), not y. At least it looks inconsistent with the direction property; polar_repr should be checked too.
math.hypot(x,y) is a faster and more accurate way for computing (x*x+y*y)**0.5 (and you do that many times).
I don't understand parts of the design, like being able to add Vector2 plus a number (and getting a number). Also, note that __bool__ isn't used in Python 2.x (use __nonzero__ instead). All Vector2 instances are always true with the current code (because of their __len__ being 2).
And no, you cannot use NamedTuple since Vector2 is mutable.
Storing x and y in a list is an optimization for most methods. I notice you do this a lot:
x, y = self.x, self.y
Which is very wise because it avoids excess attribute lookups, but if x and y were stored in a list _v, then you can do the following, which is faster still:
x, y = self._v
Trouble is, to make it nice to use you have to implement x and y as properties which are slower than attribute access, but over all its probably a win.
Please correct this assumption if it is wrong, but are not the calls to _s, _c, et cetera first hashed and then looked up in the global dictionary? If that is correct, then it should take less time to hash _s & _c instead of _sine & _cosine before looking into the dictionary's hash table. If this is not the case, I would be more than happy to rename the functions so they are easier to read.
After checking Recipe 277940, it seems that it was designed for Python 2.X, not 3.X (not that it could not be rewritten if needed). How would it be used exactly? It appears that all methods referring to global functions would need to be decorated as shown in the recipe. After figuring out the correct usage, the results can be tested with timeit to see what sort of speed it affords.
Try that last line. If our computers operate the same way, you should get back "True" from the interpreter. Writing out the math manually appears to be over twice as fast using the function from the math module. As was mentioned before, sometimes alternatives to solving problems in the code were present, but they have been tested for timing already to see what was the fastest algorithm.
What were you referring to when you asked about "being able to add Vector2 plus a number (and getting a number)"? The __add__ method has two different return statements, and both of them return new vectors with the most logical operations implemented based on the object type. Method __radd__ also returns a new vector with the addition operation swapped. Method __iadd__ simply returns self.
Check the tags. This is a Python 3.0 (3.x) recipe only.
@McGugan:
class Vector2:
__slots__ = '__a', 'x', 'y'
def __init__(self, x, y): self.__a = self.x, self.y = x, y
def test1(self): x, y = self.x, self.y
def test2(self): x, y = self.__a
After checking the speed via timeit, it appears that test2 is about %5 faster than test1 (after several runs). Considering that the code would need to be completely rewritten and that the speed increase is rather small, it may not be helpful (and particularly harmful to those who directly manipulate x & y in their algorithms). It would be nice if x & y could not be deleted, but the speedup seems too small a benefit.
Please correct this assumption if it is wrong, but are not the calls to _s, _c, et cetera first hashed and then looked up in the global dictionary?
Looking up a name takes almost constant time (specially identifiers, which are interned) regardless of their length (ok, for "reasonable" lengths...). So don't worry about _s vs sin: best to avoid the lookup itself, not shorten the identifier.
Here the time is dominated by the two name lookups (math and hypot), not by the operation itself. Accessing local names is very fast (they're looked up at compile time, and accessed thru an index into an internal array at run time). So converting global names into local ones is a common optimization:
from math import hypot
def foo(some, arguments,
_hypot=hypot):
...
magn = _hypot(x,y)
...
I am still finding test2 to take about 50% longer than test1 after running them several times. Are you sure your method is faster? If something was typed incorrectly, maybe you can find it and explain what the problem is inside the example. According to what you said, hypot as a local variable should be considerably faster than typing out the formula manually. It seems that opposite is true in this case for some odd reason.
Would anyone mind testing the two case studies above to see if the observed time difference is correct? May the timing is different on different processors. Both should be floating-point calculations, and maybe some CPUs are faster or slower than others using the same numbers. It could be that there was some misunderstanding as well.
@Genellina:
I'm sorry the question was not answered before, but the polar notation uses an idiom that seems to make the most sense to myself. If you check the properties (virtual attributes), you will find that there is a "direction" and "degrees" that can be both accessed and mutated. Direction is good when you want to set two vectors the same way.
Using the direction is faster, but it is in radians while, and the orientation is somewhat confusing (when looking on a graph). Degrees is slower because of the extra calculations, but "north" (0 degrees) is straight up on a graph. Even the turtle module confuses me. Its rotational model may be traditional but hard to understand.
The polar representation was designed to take with degrees in mind, and that is what the Polar2 function expects as well. Polar2(1, 0) == (x, y) == (0, 1) while Polar2(1, 90) == (1, 0) while Polar2(1, 180) == (0, -1) while Polar2(1, 270) == (-1, 0) et cetera. Rotation is clockwise starting from north (straight up) on the graph.
Comments
Code can be simplified a bit, if rebased upon builtin collections.NamedTuple class, which seem to be quite fitting for a vector.
Would it slow down the code at all? The module has been consistently tested for speed, and while it may be convenient and simpler (and maybe even proper) to inherit from other classes or else embed an object inside another object, that approach has been rejected in this recipe for optimum speed in pure Python code. Going against that technique may make the code harder to maintain in the long run, but once the classes are properly written, they should require no changes in the future since vector math does not vary from time to time.
Can you provide a short sample implementation using "collections.NamedTuple" that is as fast as the one presented?
Using short names like _s, _c doesn't help speed and is a lot harder to read so I'd stick with the full names sin, cos, etc.
Recipe 277940 would make things faster, and it looks you don't rely on reasignable globals so using it won't hurt.
Inside Polar2, aren't the axes inverted? In usual coordinates, x=Magnitude*cos(angle), not y. At least it looks inconsistent with the direction property; polar_repr should be checked too.
math.hypot(x,y) is a faster and more accurate way for computing
(x*x+y*y)**0.5(and you do that many times).I don't understand parts of the design, like being able to add Vector2 plus a number (and getting a number). Also, note that __bool__ isn't used in Python 2.x (use __nonzero__ instead). All Vector2 instances are always true with the current code (because of their __len__ being 2).
And no, you cannot use NamedTuple since Vector2 is mutable.
I did something similar in GameObjects http://code.google.com/p/gameobjects/source/browse/trunk/vector2.py
Storing x and y in a list is an optimization for most methods. I notice you do this a lot:
Which is very wise because it avoids excess attribute lookups, but if x and y were stored in a list _v, then you can do the following, which is faster still:
Trouble is, to make it nice to use you have to implement x and y as properties which are slower than attribute access, but over all its probably a win.
@Genellina:
Please correct this assumption if it is wrong, but are not the calls to _s, _c, et cetera first hashed and then looked up in the global dictionary? If that is correct, then it should take less time to hash _s & _c instead of _sine & _cosine before looking into the dictionary's hash table. If this is not the case, I would be more than happy to rename the functions so they are easier to read.
After checking Recipe 277940, it seems that it was designed for Python 2.X, not 3.X (not that it could not be rewritten if needed). How would it be used exactly? It appears that all methods referring to global functions would need to be decorated as shown in the recipe. After figuring out the correct usage, the results can be tested with timeit to see what sort of speed it affords.
timeit.Timer('(1.2 * 1.2 + 4.3 * 4.3) ** 0.5').timeit() < timeit.Timer('math.hypot(1.2, 4.3)', 'import math').timeit()
Try that last line. If our computers operate the same way, you should get back "True" from the interpreter. Writing out the math manually appears to be over twice as fast using the function from the math module. As was mentioned before, sometimes alternatives to solving problems in the code were present, but they have been tested for timing already to see what was the fastest algorithm.
What were you referring to when you asked about "being able to add Vector2 plus a number (and getting a number)"? The __add__ method has two different return statements, and both of them return new vectors with the most logical operations implemented based on the object type. Method __radd__ also returns a new vector with the addition operation swapped. Method __iadd__ simply returns self.
Check the tags. This is a Python 3.0 (3.x) recipe only.
@McGugan:
class Vector2: __slots__ = '__a', 'x', 'y' def __init__(self, x, y): self.__a = self.x, self.y = x, y def test1(self): x, y = self.x, self.y def test2(self): x, y = self.__a
After checking the speed via timeit, it appears that test2 is about %5 faster than test1 (after several runs). Considering that the code would need to be completely rewritten and that the speed increase is rather small, it may not be helpful (and particularly harmful to those who directly manipulate x & y in their algorithms). It would be nice if x & y could not be deleted, but the speedup seems too small a benefit.
Looking up a name takes almost constant time (specially identifiers, which are interned) regardless of their length (ok, for "reasonable" lengths...). So don't worry about _s vs sin: best to avoid the lookup itself, not shorten the identifier.
Here the time is dominated by the two name lookups (math and hypot), not by the operation itself. Accessing local names is very fast (they're looked up at compile time, and accessed thru an index into an internal array at run time). So converting global names into local ones is a common optimization:
Recipe 277940 just does it automatically for you.
2to3 almost converts it fine, but there are a couple things that must be fixed. I'll post a revised version in its comments section.
No, you may use the
make_constants()function to apply the optimization to the whole module.Ouch, I must have misread the code. Looks fine to me now, sorry the noise.
I meant: use
bind_allTimer('test1(1.2, 4.3)', 'def test1(a, b): return (a * a + b * b) ** 0.5').timeit()Timer('test2(1.2, 4.3)', 'import math\ndef test2(a, b, h=math.hypot): return h(a, b)').timeit()I am still finding test2 to take about 50% longer than test1 after running them several times. Are you sure your method is faster? If something was typed incorrectly, maybe you can find it and explain what the problem is inside the example. According to what you said, hypot as a local variable should be considerably faster than typing out the formula manually. It seems that opposite is true in this case for some odd reason.
Would anyone mind testing the two case studies above to see if the observed time difference is correct? May the timing is different on different processors. Both should be floating-point calculations, and maybe some CPUs are faster or slower than others using the same numbers. It could be that there was some misunderstanding as well.
@Genellina:
I'm sorry the question was not answered before, but the polar notation uses an idiom that seems to make the most sense to myself. If you check the properties (virtual attributes), you will find that there is a "direction" and "degrees" that can be both accessed and mutated. Direction is good when you want to set two vectors the same way.
Using the direction is faster, but it is in radians while, and the orientation is somewhat confusing (when looking on a graph). Degrees is slower because of the extra calculations, but "north" (0 degrees) is straight up on a graph. Even the turtle module confuses me. Its rotational model may be traditional but hard to understand.
The polar representation was designed to take with degrees in mind, and that is what the Polar2 function expects as well. Polar2(1, 0) == (x, y) == (0, 1) while Polar2(1, 90) == (1, 0) while Polar2(1, 180) == (0, -1) while Polar2(1, 270) == (-1, 0) et cetera. Rotation is clockwise starting from north (straight up) on the graph.
Sign in to comment