Immutable Geometry Types

P

PeterBraden1

Hi,

I am learning python, having learnt most of my object orientation with
java, and decided to port some of my geometry classes over. I haven't
used immutability in python before, so thought this would be an
interesting chance to learn.

I am looking for feedback on any ways in which I might have left
variables unprotected, and ways in which I am not being pythonic.

Cheers!

<code class = "python">
#!/usr/bin/env python
#
# geometry.py
#
# Peter Braden <http://PeterBraden.co.uk>
#
# Released under the GPLv2 (http://www.gnu.org/licenses/
gpl-2.0.txt).
#
# Disclaimer:
#
# All following code is provided "as is". Peter Braden hereby
disclaims
# solely to the extent permitted by law all express, implied and
statutory
# warranties, conditions and terms including, without limitation,
those
# regarding the security, reliability, timeliness, and performance of
the code.
#
#

import math

class Angle (object):
"""
The Angle class represents angles; the inclination to each other,
in a plane,
of two lines which meet each other, and do not lie straight with
respect to
each other.

* By default angles are measured in radians.

* Angles are immutable
"""



def __init__(self, val, type = 'rad'):
"""
Create a new angle.

* To specify type of value use either type = "deg" or type
="rad"
* default is radians
"""
if type == 'rad':
super(Angle, self).__setattr__('_value', val)
else:
super(Angle, self).__setattr__('_value',
math.radians(val))


def __eq__(self, other):
"""
Test equality
"""
if isinstance(other, Angle):
return self._value == other._value
return NotImplemented

def __ne__(self, other):
"""
Test Inequality
"""
result = self.__eq__(other)
if result is NotImplemented:
return result
return not result

def __str__(self):
"""
Create Readable String
"""
return "%s (%s degrees)" % (str(self._value),
str(self.degrees))

def __repr__(self):
"""
Serialise data
"""
return "Angle(%s)" % self._value

def __setattr__(self, name, value):
"""
Suppress setting of data - Angle is immutable
"""
self._immutableError()

def __delattr__(self, name):
"""
Suppress deleting of data - Angle is immutable
"""
self._immutableError()

def __add__(self, other):
"""
return self + other
"""
if isinstance(other, Angle):
return Angle(self._value + other._value)
return NotImplemented

def __sub__(self, other):
"""
return self - other
"""
if isinstance(other, Angle):
return Angle(self._value - other._value)
return NotImplemented

def __mul__(self, other):
"""
return self * other
"""
if isinstance(other, Angle):
return Angle(self._value * other._value)
return NotImplemented

def __div__(self, other):
"""
return self / other
"""
if isinstance(other, Angle):
return Angle(self._value / other._value)
return NotImplemented

def __lt__(self, other):
"""
return self < other
"""
if isinstance(other, Angle):
return self._value < other._value
return NotImplemented

def __gt__(self, other):
"""
return self > other
"""
if isinstance(other, Angle):
return self._value > other._value
return NotImplemented

def __le__(self, other):
"""
return self >= other
"""
if isinstance(other, Angle):
return self._value <= other._value
return NotImplemented

def __ge__(self, other):
"""
return self <= other
"""
if isinstance(other, Angle):
return self._value >= other._value
return NotImplemented


def fromCos(self, c):
"""
return angle with specified cos
"""
return Angle(math.acos(c))

fromCos = classmethod(fromCos)

def fromSin(self, s):
"""
return angle with specified sin
"""
return Angle(math.asin(c))

fromSin = classmethod(fromSin)

def fromTan(self, t):
"""
return angle with specified tan
"""
return Angle(math.atan(c))

fromTan = classmethod(fromTan)


def _immutableError(self):
"""
Throw error about angles immutability
"""
raise TypeError("Angle is immutable - cannot alter variables")

radians = property(lambda self: self._value, lambda x:
self._immutableError())
degrees = property(lambda self: math.degrees(self._value), lambda
x: self._immutableError())
cos = property(lambda self: math.cos(self._value), lambda x:
self._immutableError())
sin = property(lambda self: math.sin(self._value), lambda x:
self._immutableError())
tan = property(lambda self: math.tan(self._value), lambda x:
self._immutableError())

def withinRange(self, angle, range):
"""
angle is within range of self
"""
return (self._value < angle._value + range) and (self._value >
angle._value - range)

def isAcute(self):
"""
angle is acute?
"""
return self._value < (math.pi/2)


#Common Values

DEG_30 = Angle(math.radians(30))
DEG_60 = Angle(math.radians(60))
DEG_90 = Angle(math.radians(90))
DEG_120 = Angle(math.radians(120))
DEG_180 = Angle(math.radians(180))
DEG_270 = Angle(math.radians(270))
DEG_360 = Angle(math.radians(360))


class Point2D (object):
"""
2 dimensional point type.

* Can represent both vectors and points

* Immutable
"""

def __init__(self, x, y):
self.x = x
self.y = y

def __eq__(self, other):
if isinstance(other, Point2D):
return self.x == other.x and self.y == other.y
return NotImplemented

def __ne__(self, other):
result = self.__eq__(other)
if result is NotImplemented:
return result
return not result

def __str__(self):
return "<%s, %s>" % (str(self.x), str(self.y))

def __repr__(self):
return "Point2D(%s, %s)" % (self.x, self.y)

def __setattr__(self, name, value):
self._immutableError()

def __delattr__(self, name):
self._immutableError()

def __add__(self, other):
if isinstance(other, Point2D):
return Point2D(self.x + other.x, self.y +other.y)
return NotImplemented

def __sub__(self, other):
if isinstance(other, Point2D):
return Point2D(self.x - other.x, self.y - other.y)
return NotImplemented

def __mul__(self, other):
if isinstance(other, Point2D):
return self.x*other.x + self.y*other.y)
return NotImplemented

def __getitem__(self,index):
if index == 0:
return self.x
if index == 1:
return self.y
raise TypeError("Index out of bounds for Point2D (x is 0, y is
1)")

def __setitem__(self,index,value):
self._immutableError()

def __delitem__(self,index,value):
self._immutableError()



def _immutableError(self):
raise TypeError("Point2D is immutable - cannot alter
variables")

length = property(lambda self: return math.sqrt(self.x*self.x +
self.y*self.y), lambda x: self._immutableError())

def normalise(self):
return Point2D(self.x/self.length, self.y/self.length);

def translate(self, other) {
return self + other
}

def rotate(self, pivot, rot) {
return Point2D( ((self.x - pivot.x) * rot.cos - (self.y -
pivot.y)* rot.sin) + pivot.x,
((self.x - pivot.x)*rot.sin + (self.y -
pivot.y)*rot.cos)+pivot.y)
}


def dotProduct(self, c){
return self * c
}

def crossProductLength(self, p){
return (self.x * p.y - self.y * p.x);
}
</code>
 
G

Gabriel Genellina

En Sun, 16 Dec 2007 23:13:47 -0300, (e-mail address removed)
Hi,

I am learning python, having learnt most of my object orientation with
java, and decided to port some of my geometry classes over. I haven't
used immutability in python before, so thought this would be an
interesting chance to learn.

I am looking for feedback on any ways in which I might have left
variables unprotected, and ways in which I am not being pythonic.

Immutable classes usually implement __new__ and leave __init__
unimplemented:

def __new__(cls, val, measure='rad'):
# using 'type' as a name shadows the builtin type
instance = super(Angle, self).__new__()
if measure == 'rad': pass
elif measure == 'deg': value = math.radians(value)
else: raise ValueError, "unknown Angle measure: %r" % measure
instance._value = value
return instance

For the various comparison operators, since this class "plays well" with
ordering, it's easier to implement __cmp__ and make all operators refer to
it:

def __cmp__(self, other):
if isinstance(other, Angle):
return cmp(self._value, other._value)
raise NotImplementedError

__eq__ = lambda self,other: self.__cmp__(other)==0
__ne__ = lambda self,other: self.__cmp__(other)!=0
__lt__ = lambda self,other: self.__cmp__(other)<0
__ge__ = lambda self,other: self.__cmp__(other)>=0
__gt__ = lambda self,other: self.__cmp__(other)>0
__le__ = lambda self,other: self.__cmp__(other)<=0
def __setattr__(self, name, value):
"""
Suppress setting of data - Angle is immutable
"""
self._immutableError()

Why? This class contains a single attribute, value, and we could make it
immutable. But why restrict the possibility to add *other* attributes?
(There is __slots__ too, but I would not recommend it at this stage)
def __delattr__(self, name):

Same as above.
def __mul__(self, other):
def __div__(self, other):

(I've never seen those operations on angles)
def fromCos(self, c):
"""
return angle with specified cos
"""
return Angle(math.acos(c))

fromCos = classmethod(fromCos)

I prefer to write

@classmethod
def fromCos(self, c):
radians = property(lambda self: self._value, lambda x:
self._immutableError())
degrees = property(lambda self: math.degrees(self._value), lambda
x: self._immutableError())
cos = property(lambda self: math.cos(self._value), lambda x:
self._immutableError())
sin = property(lambda self: math.sin(self._value), lambda x:
self._immutableError())
tan = property(lambda self: math.tan(self._value), lambda x:
self._immutableError())

You don't have to write them that way. Just omit the property setter, and
it will be read-only.

tan = property(lambda self: math.tan(self._value))

Or:

@property
def tan(self):
return math.tan(self.value)
def withinRange(self, angle, range):
"""
angle is within range of self
"""
return (self._value < angle._value + range) and (self._value >
angle._value - range)

I think this is more readable:

return angle.value-range < self.value < angle.value+range
class Point2D (object):

Same as above, replace __init__ by __new__. You may inherit from tuple
instead, and get some basic methods already implemented.
length = property(lambda self: return math.sqrt(self.x*self.x +
self.y*self.y), lambda x: self._immutableError())

Using math.hypot is safer in some circunstances.
def normalise(self):
return Point2D(self.x/self.length, self.y/self.length);
def translate(self, other) {
return self + other
}

I'd use "normalised", "translated", etc. because they return a *new*
object, instead of modifying self.
(You forgot to remove some braces, I presume...)
 
I

I V

I am learning python, having learnt most of my object orientation with
java, and decided to port some of my geometry classes over. I haven't
used immutability in python before, so thought this would be an
interesting chance to learn.

I am looking for feedback on any ways in which I might have left
variables unprotected, and ways in which I am not being pythonic.

I'm not sure it's pythonic to worry too much about enforcing the
immutability; and disabling setattr makes the internal attribute setting
a pain in the arse, for little real gain.
class Angle (object):

I'm not sure about this, but you might want to make Angle a subclass of
float ; it would save you re-implementing the comparison operators
(although you'ld still have to re-implement the arithmetic operators to
get them to return an Angle instance rather than a float).
def __init__(self, val, type = 'rad'):
if type == 'rad':
super(Angle, self).__setattr__('_value', val)
else:
super(Angle, self).__setattr__('_value',
math.radians(val))

You might want to normalize the angle to between 0 and 2*pi here (though
you might not, depending on exactly what you want to represent).

Assuming that the user meant degrees in the else branch strikes me as a
mistake; what if they mis-typed "rad" as "rsd" or something? Better to
explicitly check, and raise an exception if the value isn't one of 'rad'
or 'deg'. Another thing you might want to consider is using keyword
arguments, rather than a string. Like:

def __init__(self, radians=None, degrees=None):
if radians is not None:
self._value = radians
elif degrees is not None:
self._value = math.radians(degrees)
else:
raise TypeError("Angle creation must specify \
keyword argument 'radians' or 'degrees')

Used like:
a = Angle(math.pi) # Uses radians by default
a = Angle(radians=2*math.pi) # Or you can specify it explicitly
a = Angle(degrees=180) # Or specify degrees
def __setattr__(self, name, value):
"""
Suppress setting of data - Angle is immutable """
self._immutableError()

def __delattr__(self, name):
"""
Suppress deleting of data - Angle is immutable """
self._immutableError()

As I say, I wouldn't bother with these, as they make the implementation
more annoying to no real gain.
def __add__(self, other):
if isinstance(other, Angle):
return Angle(self._value + other._value)
return NotImplemented

Using 'isinstance' is usually a mistake - rather than checking for type,
you should just use an object as if it were the correct type and, if
necessary, deal with the resulting exceptions. This means you can use
objects of a different type, as long as they have the right interface.
Here, I'd do:

def __add__(self, other):
return Angle(self.radians + other.radians)

and likewise for __sub__.
def __mul__(self, other):
"""
return self * other
"""
if isinstance(other, Angle):
return Angle(self._value * other._value)
return NotImplemented

Leaving aside my earlier point about not checking for types, does it even
make sense to multiply an angle by another angle? I would have thought
you multiplied angles by numbers. So just do:

def __mul__(self, other):
return Angle(self._value * other)

And the same for __div__

def fromCos(self, c):
"""
return angle with specified cos
"""
return Angle(math.acos(c))

fromCos = classmethod(fromCos)

You could use decorators here; and the preferred python style for method
names is all lower case, separated by underscores:

@classmethod
def from_cos(self, c):
return Angle(math.acos(c))
radians = property(lambda self: self._value, lambda x:
self._immutableError())

You don't need to explicitly raise an exception in the setter; just don't
specify a setter function, and attempting to set the property will raise
an AttributeError.
DEG_30 = Angle(math.radians(30))

Given that you've gone to the trouble of creating a constructor that will
take values as degrees, why not use it here?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,576
Members
45,054
Latest member
LucyCarper

Latest Threads

Top