The Hard-Core Coder


Home | Pages | Archives


Simple Python Tricks #11

November 11, 2024 11:11 am

Last time we looked at subclassing the Python built-in classes tuple, list, and dict with a focus on the built-in __new__ and __init__ methods (and never forget to include __str__ and/or __repr__ in your user-defined classes).

This time Simple Tricks explores many of the other built-in methods that help you create richly featured Python-aware objects. Specifically, we’ll focus on subclassing tuple to create (immutable) vector objects. A basic understanding of object-oriented programming is assumed.

Generally speaking, a vector is an array or list of numbers. They often represent a point, direction, or force in a multidimensional space. An [x, y] point on a graph is an example of a two-dimensional vector.

Here we design a vector class with the following features:

  1. Vectors can be any length, including zero.
  2. Vector elements must be numeric.
  3. Vectors are immutable.
  4. Vectors of length 1-5 have x, y, z, w, u convenience properties.
  5. Vectors of any length are indexable (e.g. vec[n]).
  6. Vectors are comparable and sortable.
  7. Vectors have a variety of math operations (detailed below).

Items #3 and #5 suggest we use tuple as our base class. Tuples are immutable and indexable, so we inherit that functionality. Tuples can be any length — and the built-in len function already works with them — so item #1 is covered. We also inherit the container functionality, the ability index and iterate.

We need to implement items #2, #4, #6, and #7. Last time, the numbers class illustrated how to implement item #2, and we’ll use it again here. This post is mainly about requirements #4, #6, and #7.

Let’s start with the basics we learned creating the point class last time. The vector class we create here expands on that, and this first part implementing the __new__ and __init__ methods is very similar:

001| from math import sqrt
002| 
003| class vector (tuple):
004|     ”’Vector class (based on tuple).”’
005|     ErrNotAVector = ‘Comparisons require two vector objects!’
006|     ErrWrongLength = ‘Vector lengths must match.’
007|     ErrBadMathType = ‘Unsupported type for vector %s: %s’
008|     ErrNoInPlaceOp = ‘Vectors are immutable. In-place operations not allowed.’
009| 
010|     def __new__ (cls, *args, **kwargs):
011|         ”’New vector instance.”’
012| 
013|         # Single arg is an iterable, multiple args make a list…
014|         arglst = list(args[0]) if len(args)==1 else args
015| 
016|         # Every member must be numeric…
017|         for ix,a in enumerate(arglst, start=1):
018|             if isinstance(a,int): continue
019|             if isinstance(a,float): continue
020|             raise ValueError(f’Illegal list item: #{ix}={a}’)
021| 
022|         # Get and return new tuple…
023|         return super().__new__(cls, arglst)
024| 
025|     def __init__ (self, *args, prec=3, sep=‘, ‘, plus=True):
026|         ”’Initialize vector instance.”’
027|         # Keep initialize chain intact…
028|         super().__init__()
029| 
030|         # Set keyword properties…
031|         self.precision = prec
032|         self.separator = sep
033|         self.plus = ‘+’ if plus else 
034| 
035|     def __str__ (self):
036|         ”’ Return pretty string version. ”’
037|         ss = [f'{a:{self.plus}.{self.precision}f}’ for a in self]
038|         return f'[{self.separator.join(ss)}]’
039| 
040|     def __repr__ (self):
041|         ”’ Return Vector as a serializable object (JSON). ”’
042|         ss = [f'”e{ix}”:{a:.6f}’ for ix,a in enumerate(self)]
043|         return f'”vector”:{{{“, “.join(ss)}}}’
044| 

Recall that, because tuples are immutable, their initialization takes place during the __new__ method, not the __init__ method. Our vectors expect either a single iterable or multiple regular (unnamed) arguments for the vector. We also support some keyword arguments, so the generic new instance signature is:

vec = vector(*args, **kwargs)

Which is all the __new__ method (lines #10 to #23) needs. It ignores kwargs and looks only at args (line #14) for a single regular argument, which must be an iterable object, or multiple arguments. Either way, arglst holds the list elements.

In lines #17 to #20 we check to ensure each item is numeric (this implements requirement #2).

In line #23 we pass our vetted list of numbers to tuple.__new__ to actually create the tuple, which we return.

The __init__ method ignores the regular arguments. The tuple is already populated, and tuple.__init__ takes no arguments. We handle the keyword arguments here. The actual signature new instance signature is:

vec = vector(*args, prec=3, sep=', ', plus=True)

The keyword parameters allow fine-tuning the string returned by __str__. The prec parameter specifies the decimal digit precision (normally 3). The sep parameter specifies the string used between elements (normally a comma and space). The plus parameter specifies whether positive values have a plus sign (normally true). The __init__ method (lines #25 to #33) just stores their values (lines #31 to #33).

This fragment also implements the two built-in string methods. The __str__ method (lines #35 to #38) uses the keyword parameters stored in __init__. The __repr__ method (lines #40 to #43), as is my habit, returns a JSON string. You should implement them however works for you.


The above is similar to the point class from last time. Now we’ll start expanding our vector class to make it more useful:

045|     def __getattr__ (self, name):
046|         ”’Read-only Properties.”’
047|         if name == ‘x’: return self[0] if 0<len(self) else None
048|         if name == ‘y’: return self[1] if 1<len(self) else None
049|         if name == ‘z’: return self[2] if 2<len(self) else None
050|         if name == ‘w’: return self[3] if 3<len(self) else None
051|         if name == ‘u’: return self[4] if 4<len(self) else None
052| 
053|     def __call__ (self): return tuple(self)
054| 
055|     def aslist (self): return list(self)
056|     def asdict (self): return {f’x{ix}’:a for ix,a in enumerate(self)}
057| 
058|     def __bool__ (self):
059|         ”’Vector is zero if all members are zero.”’
060|         return (sum(self) != 0)
061| 
062|     def props (self, prec=None, sep=None, plus=None):
063|         ”’Return (optionally set) keyword parameters.”’
064|         # Optionally set keyword values…
065|         if prec is not None:
066|             self.precision = prec
067|         if sep is not None:
068|             self.separator = sep
069|         if plus is not None:
070|             self.plus = ‘+’ if plus else 
071| 
072|         # Return current keyword properties…
073|         return dict(prec=self.precision, sep=self.separator, plus=self.plus)
074| 

The built-in __getattr__ method (lines #45 to #51) implements requirement #4 for x, y, z, w, & u properties that alias the first five elements. If a vector is too short for a given property, rather than raising an exception, we’ll return None. Arguably, raising an exception is preferred (and left as an exercise for the reader). These read-only attributes allow us to use vectors like this:

001| from vector import vector
002| 
003| vec = vector(0.51, 1.67, 2.72, 3.14, 6.63, prec=2)
004| print(vec)
005| print()
006| print(f'{vec.x = :.6f}’)
007| print(f'{vec.y = :.6f}’)
008| print(f'{vec.z = :.6f}’)
009| print(f'{vec.w = :.6f}’)
010| print(f'{vec.u = :.6f}’)
011| print()
012| 

When run, this prints:

[+0.51, +1.67, +2.72, +3.14, +6.63]

vec.x = 0.510000
vec.y = 1.670000
vec.z = 2.720000
vec.w = 3.140000
vec.u = 6.630000

The attributes are read-only because vectors are immutable.

The built-in __call__ method makes object instances callable as if they were functions. We use it in line #53 to return a tuple version. The aslist and asdict methods (lines #55 & #56) return list and dict versions, respectively. For the dictionary, keys are strings “e#”, with “#” starting at zero:

001| from vector import vector
002| 
003| vec = vector(0.51, 1.67, 2.72, 3.14, 6.63, prec=2)
004| vlist = vec.aslist()
005| vdict = vec.asdict()
006| 
007| print(f'{vec() = }’)
008| print()
009| print(f'{type(vlist) = }’)
010| for ix,item in enumerate(vlist):
011|     print(f’ {ix}: {item}’)
012| print()
013| print(f'{type(vdict) = }’)
014| for key,value in vdict.items():
015|     print(f’ {key}: {value}’)
016| print()
017| 

When run, this prints:

vec() = (0.51, 1.67, 2.72, 3.14, 6.63)

type(vlist) = <class 'list'>
  0: 0.51
  1: 1.67
  2: 2.72
  3: 3.14
  4: 6.63

type(vdict) = <class 'dict'>
  x0: 0.51
  x1: 1.67
  x2: 2.72
  x3: 3.14
  x4: 6.63

The built-in __bool__ method (lines #59 to #61) returns a Boolean value signifying whether an object is “true” or “false”. The usual treatment is that an object is “true” if it is “non-zero” and “false” if it is “zero” (with “zero” defined appropriately for the object). We’ll say a vector is “true” if any element is not zero:

001| from vector import vector
002| 
003| def test_vec (v):
004|     print(v)
005|     if v:
006|         print(‘vector is True’)
007|     else:
008|         print(‘vector is False’)
009|     print()
010| 
011| vec = vector(0.5, 1.7, 2.7, 3.1, 6.6, prec=1, plus=False)
012| test_vec(vec)
013| test_vec(vector())
014| test_vec(vector([0], prec=0, plus=False))
015| test_vec(vector([1, 0], prec=0, plus=False))
016| test_vec(vector(0,0,0, prec=0, plus=False))
017| test_vec(vector(0,0,0,0,1, prec=0, plus=False))
018| 

When run, this prints:

[0.5, 1.7, 2.7, 3.1, 6.6]
vector is True

[]
vector is False

[0]
vector is False

[1, 0]
vector is True

[0, 0, 0]
vector is False

[0, 0, 0, 0, 1]
vector is True

Lastly, the props method (lines #63 to #74) returns the keyword properties as a dictionary. It also allows changing keyword properties. As you’ll see below, its main purpose is ensuring the properties are passed on to new vector instances.


Next, we add three new methods. The magnitude method (lines #75 to #81) returns the mathematical vector length. It is implemented as a read-only property. Note that vector.magnitude and len(vector) return different values. The latter returns the number of elements, the length of the tuple, while the former returns the calculated vector length independent of the number of elements. I’ll use the term length to refer to vector magnitude and size to refer to the number of elements.

075|     @property
076|     def magnitude (self):
077|         ”’Magnitude: sqrt(sum(x_i^2)).”’
078|         # Generate squares of each member…
079|         ss = [pow(a,2) for a in self]
080|         # Return the square root of the sum of the squares…
081|         return sqrt(sum(ss))
082| 
083|     def distance (self, other):
084|         ”’Distance from one vector to another.”’
085|         # The other must be another vector…
086|         assert isinstance(other,vector), TypeError(self.ErrNotAVector)
087|         # Both vectors must have the same length…
088|         assert len(self)==len(other), ValueError(self.ErrWrongLength)
089|         # Generate squares of each element…
090|         ss = [pow(ab,2) for a,b in zip(self,other)]
091|         # Return the square root of the sum of the squares…
092|         return sqrt(sum(ss))
093| 
094|     def dot (self, other):
095|         ”’Return the dot product of two vectors. ”’
096|         # The other must be another vector…
097|         assert isinstance(other,vector), TypeError(self.ErrNotAVector)
098|         # Both vectors must have the same length…
099|         assert len(self)==len(other), ValueError(self.ErrWrongLength)
100|         # Multiply the members…
101|         ss = [a*b for a,b in zip(self,other)]
102|         # Return the sum of the products…
103|         return sum(ss)
104| 

The distance method (lines #83 to #92) takes another vector as an argument and returns the Pythagorean distance between the two points indicated by the vectors. The dot method (lines #94 to #103) returns the dot product between two vectors.

I’ve included these mainly to illustrate a read-only property and, more importantly, the need to vet input arguments. For most operations we’re expecting another vector object, which is easy to check for (lines #86 and #97). Often, that other vector must have the same size (lines #88 and #99).

Importantly, note that we must design methods to work on vectors of any size. Here’s some demonstration code:

001| from vector import vector
002| 
003| vec0 = vector(0, 0, prec=0, plus=False)
004| vec1 = vector(1, 0, prec=0, plus=False)
005| vec2 = vector(1, 1, prec=0, plus=False)
006| vec3 = vector(0, 1, prec=0, plus=False)
007| vec4 = vector(2, 5, prec=0, plus=False)
008| 
009| print(f'{vec0 = !s}’)
010| print(f'{vec1 = !s}’)
011| print(f'{vec2 = !s}’)
012| print(f'{vec3 = !s}’)
013| print(f'{vec4 = !s}’)
014| print()
015| print(f'{vec0.magnitude = !s}’)
016| print(f'{vec1.magnitude = !s}’)
017| print(f'{vec2.magnitude = !s}’)
018| print(f'{vec3.magnitude = !s}’)
019| print(f'{vec4.magnitude = !s}’)
020| print()
021| print(f'{vec0.distance(vec0) = }’)
022| print(f'{vec0.distance(vec1) = }’)
023| print(f'{vec0.distance(vec2) = }’)
024| print(f'{vec0.distance(vec3) = }’)
025| print(f'{vec0.distance(vec4) = }’)
026| print()
027| print(f'{vec1.dot(vec1) = }’)
028| print(f'{vec1.dot(vec2) = }’)
029| print(f'{vec1.dot(vec3) = }’)
030| print(f'{vec1.dot(vec4) = }’)
031| print()
032| 

When run, this prints:

vec0 = [0, 0]
vec1 = [1, 0]
vec2 = [1, 1]
vec3 = [0, 1]
vec4 = [2, 5]

vec0.magnitude = 0.0
vec1.magnitude = 1.0
vec2.magnitude = 1.4142135623730951
vec3.magnitude = 1.0
vec4.magnitude = 5.385164807134504

vec0.distance(vec0) = 0.0
vec0.distance(vec1) = 1.0
vec0.distance(vec2) = 1.4142135623730951
vec0.distance(vec3) = 1.0
vec0.distance(vec4) = 5.385164807134504

vec1.dot(vec1) = 1
vec1.dot(vec2) = 1
vec1.dot(vec3) = 0
vec1.dot(vec4) = 2

To satisfy requirement #6, we implement the built-in methods for comparing and sorting vector objects:

105|     def __eq__ (self, other):
106|         ”’Test self & other for equality.”’
107|         # The other must be another vector…
108|         assert isinstance(other,vector), TypeError(self.ErrNotAVector)
109|         # If the lengths don’t match, they can’t be equal…
110|         if len(self) != len(other):
111|             return False
112|         # Now do an element-by-element comparison…
113|         for a,b in zip(self,other):
114|             if a != b:
115|                 return False
116|         # They seem to be equal…
117|         return True
118| 
119|     def __lt__ (self, other):
120|         ”’Is self < other.”’
121|         # The other must be another vector…
122|         assert isinstance(other,vector), TypeError(self.ErrNotAVector)
123|         # Use a memberwise sort…
124|         for a,b in zip(self,other):
125|             if a < b:
126|                 return True
127|             if b < a:
128|                 return False
129|         # All elements of self equal elements in other…
130|         return False
131| 
132|     def __gt__ (self, other):
133|         ”’Is self > other.”’
134|         # The other must be another vector…
135|         assert isinstance(other,vector), TypeError(self.ErrNotAVector)
136|         # Use a memberwise sort…
137|         for a,b in zip(self,other):
138|             if a < b:
139|                 return False
140|             if b < a:
141|                 return True
142|         # All elements of self equal elements in other…
143|         return False
144| 
145|     def __ne__ (self, other): return not self.__eq__(other)
146|     def __le__ (self, other): return not self.__gt__(other)
147|     def __ge__ (self, other): return not self.__lt__(other)
148| 
149|     def __hash__ (self): return id(self)
150| 

These implement behaviors for the comparison operators, == (equals), != (not equals), < (less than), <= (less than or equal), >= (greater than or equal), and > (greater than). Note how, in three cases, we only need to use the negation of one of the other three (lines #145 to #147).

We also implement the __hash__ method (line #149) because (a) vectors are immutable and (b) we implemented the equality operator. (See the links for details.)

Some demonstration code:

001| from vector import vector
002| 
003| vec0 = vector([0]*3, prec=0)
004| vec1 = vector(0.5, 1.6, 2.7, prec=1)
005| vec2 = vector(0.5, +1.6, 2.7, prec=1)
006| vec3 = vector(1/2, +3/4, 3/7, prec=1)
007| 
008| print(f’vec0: {vec0}’)
009| print(f’vec1: {vec1}’)
010| print(f’vec2: {vec2}’)
011| print(f’vec3: {vec3}’)
012| print()
013| print(f'{hash(vec0) = }’)
014| print(f'{hash(vec1) = }’)
015| print(f'{hash(vec2) = }’)
016| print(f'{hash(vec3) = }’)
017| print()
018| print(f'{vec1 == vec1 = }’)
019| print(f'{vec1 == vec2 = }’)
020| print()
021| print(f'{vec1 != vec1 = }’)
022| print(f'{vec1 != vec2 = }’)
023| print()
024| print(f'{vec1 < vec1 = }’)
025| print(f'{vec1 < vec2 = }’)
026| print()
027| print(f'{vec1 <= vec1 = }’)
028| print(f'{vec1 <= vec2 = }’)
029| print()
030| print(f'{vec1 > vec1 = }’)
031| print(f'{vec1 > vec2 = }’)
032| print()
033| print(f'{vec1 >= vec1 = }’)
034| print(f'{vec1 >= vec2 = }’)
035| print()
036| 

When run, this prints:

vec0: [+0, +0, +0]
vec1: [+0.5, -1.6, -2.7]
vec2: [+0.5, +1.6, -2.7]
vec3: [+0.5, +0.8, -0.4]

hash(vec0) = 2429128483648
hash(vec1) = 2429143898832
hash(vec2) = 2429144218608
hash(vec3) = 2429144215008

vec1 == vec1 = True
vec1 == vec2 = False

vec1 != vec1 = False
vec1 != vec2 = True

vec1 < vec1 = False
vec1 < vec2 = True

vec1 <= vec1 = True
vec1 <= vec2 = True 
vec1 > vec1 = False
vec1 > vec2 = False

vec1 >= vec1 = True
vec1 >= vec2 = False

And we can sort a list of random vectors:

001| from random import random
002| from vector import vector
003| 
004| randelem = lambda: 10*random()
005| randvect = lambda: vector(randelem(),randelem(),randelem())
006| 
007| vs = [randvect() for _ in range(16)]
008| 
009| for v in sorted(vs):
010|     print(v)
011| print()
012| 

When run, this prints:

[+0.210, +8.186, +2.376]
[+0.523, +1.100, +5.871]
[+1.895, +2.509, +8.288]
[+2.354, +0.233, +8.390]
[+2.576, +3.788, +2.408]
[+2.936, +2.993, +3.999]
[+3.070, +7.862, +9.898]
[+4.963, +7.484, +6.885]
[+5.648, +4.441, +7.105]
[+6.658, +2.237, +7.883]
[+7.119, +0.220, +1.852]
[+7.661, +7.434, +0.449]
[+8.151, +7.857, +0.038]
[+8.555, +8.912, +4.952]
[+8.967, +6.281, +2.075]
[+9.885, +4.443, +4.133]

If you run that code fragment yourself, you’ll get different numbers, of course, but they will be (lexically) sorted.


Lastly, for requirement #7, we’ll implement some mathematical operators, starting with the unary operators:

151|     def __pos__ (self):
152|         ”’Operator +: creates a copy.”’
153|         return vector([a for a in self], **self.props())
154| 
155|     def __neg__ (self):
156|         ”’Operator -: memberwise negation.”’
157|         return vector([a for a in self], **self.props())
158| 
159|     def __abs__ (self):
160|         ”’Operator abs: memberwise absolute value.”’
161|         return vector([abs(a) for a in self], **self.props())
162| 
163|     def __invert__ (self):
164|         ”’Operator ~: invert members.”’
165|         return vector([(1/a if a else 0) for a in self], **self.props())
166| 

These implement behaviors for the unary plus (+vec), the unary minus (-vec), the abs (absolute value) function, and the unary bitwise invert operator (~vec). These all return new vector objects (and you see now how the props method comes in handy ensuring the new objects inherit the property settings).

The unary plus (lines #151 to #153) returns an unaltered copy. The unary minus (lines #155 to #157) returns a copy with signs reversed on each element. The abs method (lines #159 to #161) returns a copy with each element forced to a positive value. The unary invert (lines #163 to #165) returns a copy with each element inverted (that is 1/value).

A demonstration:

001| from vector import vector
002| 
003| def demo_unary_ops (v):
004|     v1 = +v
005|     v2 = v
006|     v3 = abs(v)
007|     v4 = ~v
008|     print(f’vector : {v} id={id(v)}’)
009|     print(f’unary plus : {v1} id={id(v1)}’)
010|     print(f’unary minus : {v2} id={id(v2)}’)
011|     print(f’abs() value : {v3} id={id(v3)}’)
012|     print(f’unary invert: {v4} id={id(v4)}’)
013|     print()
014| 
015| demo_unary_ops(vector(1/3, 1/5, +1/7, prec=6))
016| demo_unary_ops(vector(2/3, +3/5, 5/7, prec=6))
017| 

Which prints:

vector      : [-0.333333, -0.200000, +0.142857] id=2372540375872
unary plus  : [-0.333333, -0.200000, +0.142857] id=2372555528912
unary minus : [+0.333333, +0.200000, -0.142857] id=2372555848768
abs() value : [+0.333333, +0.200000, +0.142857] id=2372555845088
unary invert: [-3.000000, -5.000000, +7.000000] id=2372555848848

vector      : [-0.666667, +0.600000, -0.714286] id=2372540375872
unary plus  : [-0.666667, +0.600000, -0.714286] id=2372555528912
unary minus : [+0.666667, -0.600000, +0.714286] id=2372555848848
abs() value : [+0.666667, +0.600000, +0.714286] id=2372555845088
unary invert: [-1.500000, +1.666667, -1.400000] id=2372555848768

Note how each vector is a separate object (their ids don’t match).


Next, we implement the basic four binary math operations:

167|     def _is_numeric_type (self, obj):
168|         ”’Is object a valid numeric type?”’
169|         if isinstance(obj, int): return True
170|         if isinstance(obj, float): return True
171|         # Nope…
172|         return False
173| 
174|     def __add__ (self, other):
175|         ”’Vector or Scalar Addition.”’
176|         # Memberwise add…
177|         if isinstance(other,vector):
178|             return vector([a+b for a,b in zip(self,other)], **self.props())
179| 
180|         # Add a scalar value to each member…
181|         if self._is_numeric_type(other):
182|             return vector([a+other for a in self], **self.props())
183| 
184|         raise TypeError(self.ErrBadMathType % (‘addition’, type(other)))
185| 
186|     def __sub__ (self, other):
187|         ”’Vector or Scalar Subtration.”’
188|         # Memberwise subtract…
189|         if isinstance(other,vector):
190|             return vector([ab for a,b in zip(self,other)], **self.props())
191| 
192|         # Subtract a scalar value from each member…
193|         if self._is_numeric_type(other):
194|             return vector([aother for a in self], **self.props())
195| 
196|         raise TypeError(self.ErrBadMathType % (‘subtraction’, type(other)))
197| 
198|     def __mul__ (self, other):
199|         ”’Vector or Scalar Multiplication.”’
200|         # Memberwise multiply…
201|         if isinstance(other,vector):
202|             return vector([a*b for a,b in zip(self,other)], **self.props())
203| 
204|         # Multiply each member by a scalar value…
205|         if self._is_numeric_type(other):
206|             return vector([a*other for a in self], **self.props())
207| 
208|         raise TypeError(self.ErrBadMathType % (‘multiplication’, type(other)))
209| 
210|     def __truediv__ (self, other):
211|         ”’Vector or Scalar Division.”’
212|         # Memberwise divide…
213|         if isinstance(other,vector):
214|             return vector([a/b for a,b in zip(self,other)], **self.props())
215| 
216|         # Divide each member by a scalar value…
217|         if self._is_numeric_type(other):
218|             return vector([a/other for a in self], **self.props())
219| 
220|         raise TypeError(self.ErrBadMathType % (‘division’, type(other)))
221| 

These implement the basic binary operations plus (a+b), minus (a-b), multiplication (a*b), and division (a/b). Note the Python has two built-in division methods, __truediv__ and __floordiv__. The former is the traditional division operation; the latter is for integer division (a//b), which is equivalent to floor(a/b). There are many other binary operations we could implement [see Emulating numeric types for details].

For our vector objects, we want to consider four scenarios:

vector1 operator vector2
vector operator number
number operator vector
vector =operator number

The first case performs the operation element-by-element on two vectors. The second and third cases apply the single number to each element. The last case, the “in-place” version (a+=b), isn’t allowed because vectors are immutable. A final consideration is what we mean (and allow) by number.

The binary math methods all check the other parameter to see if it’s a vector. If so, they do an element-by-element operation. If not, they use the _is_numeric_type method (lines #167 to #172) to determine if the other parameter is a valid numeric type (int or float). If it is, they apply it to each element of the vector. Either way, they return a new vector object. The methods all raise a TypeError if both tests fail.

For the third use case, we implement the “right-hand” versions of the operators we’re defining. These are invoked when the left-hand side doesn’t handle the right-hand type (integers, for instance, don’t know how to handle vectors). Python checks to see if the right-hand object has an “r” version of the operation and, if so, calls it. We’ll implement all four basic math operations for right-hand side operations:

222|     def __radd__ (self, other):
223|         ”’RHS Scalar Addition.”’
224|         return self.__add__(other)
225| 
226|     def __rsub__ (self, other):
227|         ”’RHS Scalar Subtraction.”’
228|         # Subtract a scalar value from each member…
229|         if self._is_numeric_type(other):
230|             return vector([othera for a in self], **self.props())
231| 
232|         raise TypeError(self.ErrBadMathType % (‘subtraction’, type(other)))
233| 
234|     def __rmul__ (self, other):
235|         ”’RHS Scalar Multiplication.”’
236|         return self.__mul__(other)
237| 
238|     def __rtruediv__ (self, other):
239|         ”’RHS Scalar Division.”’
240|         # Divide each member by a scalar value…
241|         if self._is_numeric_type(other):
242|             return vector([other/a for a in self], **self.props())
243| 
244|         raise TypeError(self.ErrBadMathType % (‘division’, type(other)))
245| 

Note that these have the same names as their “left-hand” counterparts but with an “r” prepended to the name. Python calls these methods only when the left-hand object is not a vector, so we know other cannot be one. That means we only need to test for a numeric type. Because add and multiply commute, we can delegate to the regular methods (lines #222 to #224 and ##234 to #236).

Subtraction and division do not commute, so we need to implement the operations here (lines #226 to #232 and #238 to #244).

The final basic math methods are the “in-place” methods (the fourth use case), and since vectors are immutable, this operation is disallowed. Using any of these raises a NotImplemented exception.

246|     def __iadd__ (self, other):
247|         ”’In-place Addition.”’
248|         raise NotImplementedError(self.ErrNoInPlaceOp)
249| 
250|     def __isub__ (self, other):
251|         ”’In-place Subtraction.”’
252|         raise NotImplementedError(self.ErrNoInPlaceOp)
253| 
254|     def __imul__ (self, other):
255|         ”’In-place Multiplication.”’
256|         raise NotImplementedError(self.ErrNoInPlaceOp)
257| 
258|     def __itruediv__ (self, other):
259|         ”’In-place Division.”’
260|         raise NotImplementedError(self.ErrNoInPlaceOp)
261| 

Note that without implementing these, the following appears to work but probably with an unintended effect:

001| from vector import vector
002| 
003| v1 = vector(2.71828, 3.14159, 6.62607, prec=2)
004| v2 = vector(1, 1, 1, prec=0)
005| 
006| print(f'{v1 = !s}, id={id(v1)}’)
007| print(f'{v2 = !s}’)
008| print()
009| 
010| v1 += v2
011| 
012| print(f'{v1 = !s}, id={id(v1)}’)
013| print()
014| 

When run, this prints:

v1 = [+2.72, +3.14, +6.63], id=2693111110352
v2 = [+1, +1, +1]

v1 = [+3.72, +4.14, +7.63], id=2693111430208

The add operation worked. It added v2 to v1, but then it assigned the resulting new vector to v1 (note how the ids are different). It’s the equivalent of:

001| from vector import vector
002| 
003| v1 = vector(2.71828, 3.14159, 6.62607, prec=2)
004| v2 = vector(1, 1, 1, prec=0)
005| 
006| print(f'{v1 = !s}, id={id(v1)}’)
007| print(f'{v2 = !s}’)
008| print()
009| 
010| v1 = (v1 + v2)
011| 
012| print(f'{v1 = !s}, id={id(v1)}’)
013| print()
014| 

Which prints the same thing. But the in-place operators imply modification of the existing object. What happens instead is a side effect of dealing with immutable objects.

Here’s some code to demonstrate the math methods:

001| from vector import vector
002| 
003| def demo_binary_ops (a, b):
004|     print(f’vector-a (self) = {a}’)
005|     print(f’vector-b (other) = {b}’)
006|     print()
007|     print(f'{a + b = !s}’)
008|     print(f'{a – b = !s}’)
009|     print(f'{a * b = !s}’)
010|     print(f'{a / b = !s}’)
011|     print()
012|     print(f'{a + 2 = !s}’)
013|     print(f'{2 + a = !s}’)
014|     print()
015|     print(f'{a – 2 = !s}’)
016|     print(f'{2 – a = !s}’)
017|     print()
018|     print(f'{a * 2 = !s}’)
019|     print(f'{2 * a = !s}’)
020|     print()
021|     print(f'{a / 2 = !s}’)
022|     print(f'{2 / a = !s}’)
023|     print(f”)
024|     print()
025|     print()
026| 
027| v1 = vector(2.71828, 3.14159, 6.62607, prec=6)
028| v2 = vector(1.00000, 2.00000, 3.00000, prec=6)
029| v3 = vector(0.50000, 0.25000, 0.12500, prec=6)
030| 
031| demo_binary_ops(v1, v2)
032| demo_binary_ops(v1, v3)
033| demo_binary_ops(v2, v3)
034| 

When run, this prints:

vector-a (self)  = [+2.718280, +3.141590, +6.626070]
vector-b (other) = [+1.000000, +2.000000, +3.000000]

a + b = [+3.718280, +5.141590, +9.626070]
a - b = [+1.718280, +1.141590, +3.626070]
a * b = [+2.718280, +6.283180, +19.878210]
a / b = [+2.718280, +1.570795, +2.208690]

a + 2 = [+4.718280, +5.141590, +8.626070]
2 + a = [+4.718280, +5.141590, +8.626070]

a - 2 = [+0.718280, +1.141590, +4.626070]
2 - a = [-0.718280, -1.141590, -4.626070]

a * 2 = [+5.436560, +6.283180, +13.252140]
2 * a = [+5.436560, +6.283180, +13.252140]

a / 2 = [+1.359140, +1.570795, +3.313035]
2 / a = [+0.735759, +0.636620, +0.301838]


vector-a (self)  = [+2.718280, +3.141590, +6.626070]
vector-b (other) = [+0.500000, +0.250000, +0.125000]

a + b = [+3.218280, +3.391590, +6.751070]
a - b = [+2.218280, +2.891590, +6.501070]
a * b = [+1.359140, +0.785397, +0.828259]
a / b = [+5.436560, +12.566360, +53.008560]

a + 2 = [+4.718280, +5.141590, +8.626070]
2 + a = [+4.718280, +5.141590, +8.626070]

a - 2 = [+0.718280, +1.141590, +4.626070]
2 - a = [-0.718280, -1.141590, -4.626070]

a * 2 = [+5.436560, +6.283180, +13.252140]
2 * a = [+5.436560, +6.283180, +13.252140]

a / 2 = [+1.359140, +1.570795, +3.313035]
2 / a = [+0.735759, +0.636620, +0.301838]


vector-a (self)  = [+1.000000, +2.000000, +3.000000]
vector-b (other) = [+0.500000, +0.250000, +0.125000]

a + b = [+1.500000, +2.250000, +3.125000]
a - b = [+0.500000, +1.750000, +2.875000]
a * b = [+0.500000, +0.500000, +0.375000]
a / b = [+2.000000, +8.000000, +24.000000]

a + 2 = [+3.000000, +4.000000, +5.000000]
2 + a = [+3.000000, +4.000000, +5.000000]

a - 2 = [-1.000000, +0.000000, +1.000000]
2 - a = [+1.000000, +0.000000, -1.000000]

a * 2 = [+2.000000, +4.000000, +6.000000]
2 * a = [+2.000000, +4.000000, +6.000000]

a / 2 = [+0.500000, +1.000000, +1.500000]
2 / a = [+2.000000, +1.000000, +0.666667]

Because this is so long, I haven’t addressed the many changes and additions that would improve the vector class. This only illustrates the basics.

For instance, I mentioned above that the __getattr__ method should probably raise an exception rather than return None. More critically, there is no matching __setattr__ method. Without it, this works:

001| from vector import vector
002| 
003| vec = vector(1.6, 2.7)
004| 
005| print(f'{vec = !s}’)
006| print(f'{vec.x = }’)
007| print(f'{vec.y = }’)
008| print(f'{vec.z = }’)
009| print()
010| 
011| vec.x = 42
012| vec.z = 21
013| print(f'{vec = !s}’)
014| print(f'{vec.x = }’)
015| print(f'{vec.y = }’)
016| print(f'{vec.z = }’)
017| print()
018| 

Because user-defined classes allow new attributes by default. When run, this prints:

vec = [+1.600, +2.700]
vec.x = 1.6
vec.y = 2.7
vec.z = None

vec = [+1.600, +2.700]
vec.x = 42
vec.y = 2.7
vec.z = 21

We apparently modified vec.x and added vec.z but didn’t affect the vector elements. There are ways around this that I’ll next time (and see the vector class in the linked ZIP file below).

Link: Zip file containing all code fragments used in this post.

Posted by Wyrd Smythe

Categories: Python

Tags: , ,

2 Responses to “Simple Python Tricks #11”

  1. ATTENTION: The WordPress Reader strips the style information from posts, which can destroy certain important formatting elements. If you’re reading this in the Reader, I highly recommend (and urge) you to [A] stop using the Reader and [B] always read blog posts on their website.

    This post is: Simple Python Tricks #11

    By Wyrd Smythe on November 11, 2024 at 11:20 am

  2. […] The last two Simple Tricks posts looked at subclasses of built-in Python classes, in particular the tuple class, as well as the built-in class methods Python supports for any user-defined class. [See Simple Tricks #10 and Simple Tricks #11.] […]

    By Simple Python Tricks #12 | The Hard-Core Coder on November 25, 2024 at 9:16 am

Leave a Reply

You must be logged in to post a comment.



Mobile Site | Full Site


Get a free blog at WordPress.com Theme: WordPress Mobile Edition by Alex King.