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.]
This time I narrow the focus to real and virtual object attributes, the “x”, “y”, “z” elements of our vector objects. Python offers many ways to implement these, depending (as always) on what you want.
To simplify the discussion, a vector instance here always has three, and only three, elements, the canonical “x”, “y”, and “z”. In other words, we’re dealing with 3D vectors.
As before, we’ll subclass the tuple object so vectors will be immutable and inherit the indexing and length behaviors of sequence types. But I’ll start with a simpler user-defined class that implements the three vector elements explicitly:
002|
003| class vector0:
004| def __init__ (self, x=0.0, y=0.0, z=0.0, fmt=‘.2f’):
005| self.x = x
006| self.y = y
007| self.z = z
008| self.fmt = fmt
009|
010| def __repr__ (self):
011| f = self.fmt
012| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
013|
014| v = vector0(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
015| print(f'{v = }’)
016| print(f'{v.x = }’)
017| print(f'{v.y = }’)
018| print(f'{v.z = }’)
019| print()
020|
021| try:
022| print(f'{v.w = }’)
023| except Exception as e:
024| print(e, file=stderr)
025| print()
026|
027| v.x = 42
028| print(f'{v = }’)
029| print(f'{v.x = }’)
030| print()
031|
032| v.a = 21
033| print(f'{v = }’)
034| print(f'{v.a = }’)
035| print()
036|
037| try:
038| del v.z
039| print(f'{v = }’)
040| except Exception as e:
041| print(e, file=stderr)
042| print()
043|
The vector0 class (lines #3 to #12) defines self.x, self.y, and self.z instance attributes, and provides default values. There is also a fmt (format) parameter that controls the string returned by the __repr__ method (lines #10 to #12).
(Remember, you should always define __repr__ and/or __str__.)
The rest of the code creates and exercises a new instance. I used try-except blocks to isolate code expected to raise an exception. Lines #14 to #19 create and print the new vector object. Lines #21 to #25 attempt to print an attribute that doesn’t exist. Lines #27 to #30 (successfully) assign a new value to the x element. Lines #32 to #35 create and print a new attribute. Finally, lines #37 to #42 attempts to delete a defined attribute (which succeeds) and then print it.
When run, this prints:
v = [2.718, 3.142, 6.626] v.x = 2.71828 v.y = 3.14159 v.z = 6.62607 'vector0' object has no attribute 'w' v = [42.000, 3.142, 6.626] v.x = 42 v = [42.000, 3.142, 6.626] v.a = 21 'vector0' object has no attribute 'z'
As expected, trying to access an attribute that doesn’t exist correctly raises an exception. Given the class design, we’d expect assigning a new value to x to work (but recall that we want our vectors to be immutable, so this is not correct behavior). We’re also free to assign new attributes to given instances. That’s the usual default behavior, but — as you’ll see below — we can interdict it if we want. Finally, because we deleted the z element, accessing it raises the same error as with any non-existent attribute.
We can do better, but first I want to move all that vector exercise code — which is the bulk of the code above — into its own function. I want to add a bit more to the exercise, so all the more reason to make it a function. It would otherwise add a lot of code to the examples below.
Here’s our vector exercise function:
002|
003| def exercise_vector (v):
004| ”’Exercise a vector object.”’
005|
006| # Print the vector and its elements…
007| print(f'{v = }’)
008| print()
009| print(f'{v.x = }’)
010| print(f'{v.y = }’)
011| print(f'{v.z = }’)
012| print()
013|
014| # Access an element that doesn’t exist…
015| try:
016| print(f'{v.w = }’)
017| except Exception as e:
018| print(e, file=stderr)
019| print()
020|
021| # Set an element to a new value…
022| try:
023| v.x = 42
024| print(f'{v.x = }’)
025| except Exception as e:
026| print(e, file=stderr)
027| print()
028|
029| # Add a new attribute…
030| try:
031| v.a = 21
032| print(f'{v.a = }’)
033| except Exception as e:
034| print(e, file=stderr)
035| print()
036|
037| # Delete an existing attribute…
038| try:
039| del v.z
040| print(f'{v = }’)
041| except Exception as e:
042| print(e, file=stderr)
043| print()
044|
045| # Set an indexed element to a new value…
046| try:
047| v[0] = 42
048| print(f'{v.x = }’)
049| except Exception as e:
050| print(e, file=stderr)
051| print()
052|
053| # Delete an indexed element…
054| try:
055| del v[1]
056| print(f'{v = }’)
057| except Exception as e:
058| print(e, file=stderr)
059| print()
060|
The comments should make it self-explanatory. We’ll try to access an element that doesn’t exist, we’ll try two ways of changing an element’s value, and we’ll try both adding new and deleting existing elements.
The problem with our vector0 class was that the attributes were directly accessible. Normally, a class design should hide the data attributes and control access. Here’s a different implementation of vector0 that does that. Most importantly, it introduces the built-in methods used for customized attribute access:
002|
003| class vector1:
004| def __init__ (self, x=0.0, y=0.0, z=0.0, fmt=‘.2f’):
005| self._x = x
006| self._y = y
007| self._z = z
008| self._fmt = fmt
009|
010| def __getattribute__ (self, name):
011| ”’Get Attribute.”’
012| print(f’getattribute({name})’)
013|
014| return super().__getattribute__(name)
015|
016| def __getattr__ (self, name):
017| ”’Read-only Properties.”’
018| print(f’getattr({name})’)
019|
020| if name == ‘x’: return self._x
021| if name == ‘y’: return self._y
022| if name == ‘z’: return self._z
023|
024| super().__getattr__(name)
025|
026| def __setattr__ (self, name, value):
027| ”’No setting of attributes allowed.”’
028| print(f’setattr({name}, {value})’)
029|
030| if name in [‘x’, ‘y’, ‘z’]:
031| raise NotImplementedError(‘Attributes are read-only.’)
032|
033| super().__setattr__(name, value)
034|
035| def __delattr__ (self, name):
036| ”’No deleting of attributes allowed.”’
037| print(f’delattr({name})’)
038|
039| if name in [‘x’, ‘y’, ‘z’]:
040| raise NotImplementedError(‘Attributes are read-only.’)
041|
042| super().__delattr__(name)
043|
044| def __repr__ (self):
045| f = self._fmt
046| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
047|
048|
049| v = vector1(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
050| print()
051| exercise_vector(v)
052|
The vector1.__init__ method is similar to the one for vector0, but here the instance attributes have a leading underscore. This is the conventional way of saying an attribute or method is private to the class. Note that some designers believe Python classes should (almost) always take this approach, but (as always) it depends.
What matters here are the four built-in methods for access to instance properties:
Python always calls the __getattribute__ method (lines #10 to #14) to get the value of a property or method. The return value is the (possibly computed) value of the property or a reference to the method. If this method fails, it should raise AttributeError — which is expected when a property or method does not exist. Most user classes won’t need to implement __getattribute__. It’s included here for completeness and just passes control to the parent (line #14).
Python calls the __getattr__ method (lines #16 to #24) if __getattribute__ raises AttributeError. Generally speaking, in alignment with the next two methods, this is the method usually implemented for virtual properties. We use the value of name to return one of the three vector elements (lines #20 to #22). Any other name we pass to the parent (line #24).
Python calls the __setattr__ method (lines #26 to #33) to set a property value. This includes creating them (which always involves setting them). We want to prevent modifying vector elements, but allow setting others, so we check the name and raise NotImplemented if it’s an element name (lines #30 and #31). Otherwise, we pass the name and value to the parent (line #33).
Python calls the __delattr__ method (lines #35 to #42) when deleting a property. The treatment here is nearly identical to that for __setattr__.
Each of these four methods has a print statement to document when Python calls them. When run, this prints (line numbers added for reference):
01|setattr(_x, 2.71828) 02|setattr(_y, 3.14159) 03|setattr(_z, 6.62607) 04|setattr(_fmt, .3f) 05| 06|getattribute(_fmt) 07|getattribute(x) 08|getattr(x) 09|getattribute(_x) 10|getattribute(y) 11|getattr(y) 12|getattribute(_y) 13|getattribute(z) 14|getattr(z) 15|getattribute(_z) 16|v = [2.718, 3.142, 6.626] 17| 18|getattribute(x) 19|getattr(x) 20|getattribute(_x) 21|v.x = 2.71828 22|getattribute(y) 23|getattr(y) 24|getattribute(_y) 25|v.y = 3.14159 26|getattribute(z) 27|getattr(z) 28|getattribute(_z) 29|v.z = 6.62607 30| 31|getattribute(w) 32|getattr(w) 33|'super' object has no attribute '__getattr__' 34| 35|setattr(x, 42) 36|Attributes are read-only. 37| 38|setattr(a, 21) 39|getattribute(a) 40|v.a = 21 41| 42|delattr(z) 43|Attributes are read-only. 44| 45|'vector1' object does not support item assignment 46| 47|'vector1' object doesn't support item deletion
The first four lines come from creating the four properties in the __init__ method. The first thing the exercise_vector function does is print the vector, which invokes its __repr__ method (code lines #44 to #46), which reads all four of the vector properties in preparing a return string. In the output (lines #6 to #15), note how the _fmt property exists, so __getattribute__ returns it. But no x or y or z property actually exists, so __getattribute__ fails, which makes Python call __getattr__. Each call to __getattr__ causes a call to __getattribute__ to get the corresponding _x or _y or _z property. Finally, we get and print the result string (line #16). It’s a bit twisty but stick with it.
Next, exercise_vector prints the individual properties (lines #18 to #29). These work the same as discussed above.
When exercise_vector tries to access a property that does exist, __getattr__ fails. Note that we let Python raise the exception in this case (line #33). The code also fails trying to modify the value of a vector element (lines #35 and #36), but __setattr__ raises the exception. Likewise, __delattr__ raises an exception when the exercise function tries to delete an element (lines #42 and #43).
The test adds a new property without issue (lines #38 to #40). The last two tests expect a sequence object, and Python raises an exception because vector1 objects don’t support any sequence methods.
This example, with its print statements, is intended for exploring how these methods work.
Speaking of sequence objects, before we subclass the immutable tuple, let’s create a vector class that subclasses list:
002|
003| class vector2 (list):
004| ElementNames = [‘x’, ‘y’, ‘z’]
005|
006| def __init__ (self, x=0.0, y=0.0, z=0.0, fmt=‘.2f’):
007| ”’Initialize new vector.”’
008| super().__init__([x,y,z])
009| self._fmt = fmt
010|
011| def __getattribute__ (self, name):
012| ”’Get Attribute.”’
013| return super().__getattribute__(name)
014|
015| def __getattr__ (self, name):
016| ”’Read-only Properties.”’
017| if name in self.ElementNames:
018| ix = self.ElementNames.index(name)
019| return self[ix]
020|
021| super().__getattr__(name)
022|
023| def __setattr__ (self, name, value):
024| ”’No setting of attributes allowed.”’
025| if name in self.ElementNames:
026| raise NotImplementedError(‘Attributes are read-only.’)
027|
028| super().__setattr__(name, value)
029|
030| def __delattr__ (self, name):
031| ”’No deleting of attributes allowed.”’
032| if name in self.ElementNames:
033| raise NotImplementedError(‘Attributes are read-only.’)
034|
035| super().__delattr__(name)
036|
037| def __repr__ (self):
038| f = self._fmt
039| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
040|
041| v = vector2(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
042| print()
043| exercise_vector(v)
044|
This class is similar to vector1 except that it uses the underlying list to store the elements rather than named instance properties. The ElementNames class variable (line #4) is another change. It’s a list of virtual property names. The attribute methods use this to determine whether name refers to a property. This avoids having to repeat the names in each method.
The __init__ method still takes explicit x, y, and z parameters but combines them into a list to initialize the parent list object with those values. The only property now is the self._fmt property used by the __repr__ method.
The instrumentation print statements from vector1 have all been removed for clearer output. When run, this prints:
v = [2.718, 3.142, 6.626] v.x = 2.71828 v.y = 3.14159 v.z = 6.62607 'super' object has no attribute '__getattr__' Attributes are read-only. v.a = 21 Attributes are read-only. v.x = 42 list index out of range
The first thing to notice is that our attempt to call the parent.__getattr__ method fails. The list class doesn’t define this method, so we should remove line #21 and replace it with a line that raises an exception:
016| ”’Read-only Properties.”’
017| if name in self.ElementNames:
018| ix = self.ElementNames.index(name)
019| return self[ix]
020|
021| #super().__getattr__(name) <<– no such method
021| raise AttributeError(f’No such attribute “{name}”‘)
022|
The second thing is that we can see that our virtual attributes are protected, they can’t be modified or deleted, but any properties the user adds work as expected.
However, the third thing is our vector can be modified through the mutable list. We could override the built-in __getitem__ and __setitem__ and __delitem__ methods (see last example below), but it’s easier to use the immutable tuple class (creating our own sequence class is a post for another time).
Here, finally, is a vector implementation that subclasses tuple:
002|
003| class vector3 (tuple):
004| ElementNames = [‘x’, ‘y’, ‘z’]
005|
006| def __new__ (cls, x=0.0, y=0.0, z=0.0, **kwargs):
007| ”’New vector instance.”’
008|
009| # Get and return new tuple…
010| return super().__new__(cls, [x,y,z])
011|
012| def __init__ (self, x=0.0, y=0.0, z=0.0, fmt=‘.2f’):
013| ”’Initialize vector instance.”’
014| super().__init__()
015|
016| self._fmt = fmt
017|
018| def __getattribute__ (self, name):
019| ”’Get Attribute.”’
020| return super().__getattribute__(name)
021|
022| def __getattr__ (self, name):
023| ”’Read-only Properties.”’
024| if name in self.ElementNames:
025| ix = self.ElementNames.index(name)
026| return self[ix]
027| raise NotImplementedError(f’No such Attribute “{name}”.’)
028|
029| def __setattr__ (self, name, value):
030| ”’No setting of attributes allowed.”’
031| if name in [‘x’, ‘y’, ‘z’]:
032| raise NotImplementedError(‘Attributes are read-only.’)
033|
034| super().__setattr__(name, value)
035|
036| def __delattr__ (self, name):
037| ”’No deleting of attributes allowed.”’
038| if name in [‘x’, ‘y’, ‘z’]:
039| raise NotImplementedError(‘Attributes are read-only.’)
040|
041| super().__delattr__(name)
042|
043| def __repr__ (self):
044| f = self._fmt
045| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
046|
047| v = vector3(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
048| print()
049| exercise_vector(v)
050|
Because tuples are immutable, we must implement the __new__ method (lines #6 to #10). See previous post for details. We should validate for numeric values, but I left that out for brevity. The rest is essentially identical to vector2.
The key difference shows up when we run it:
v = [2.718, 3.142, 6.626] v.x = 2.71828 v.y = 3.14159 v.z = 6.62607 No such Attribute "w". Attributes are read-only. v.a = 21 Attributes are read-only. 'vector3' object does not support item assignment 'vector3' object doesn't support item deletion
It’s no longer possible to modify elements using the underlying object.
We now have a useful vector class that solves key issues from the version in the previous post. But we can do even better. Or perhaps at least be more Pythonish.
The first improvement is that we can use Python’s @property decorator:
002|
003| class vector4 (tuple):
004| def __new__ (cls, x=0.0, y=0.0, z=0.0, **kwargs):
005| ”’New vector instance.”’
006| return super().__new__(cls, [x,y,z])
007|
008| def __init__ (self, *args, fmt=‘.2f’):
009| ”’Initialize vector instance.”’
010| super().__init__()
011| self._fmt = fmt
012|
013| @property
014| def x (self): return self[0]
015|
016| @property
017| def y (self): return self[1]
018|
019| @property
020| def z (self): return self[2]
021|
022| def __repr__ (self):
023| f = self._fmt
024| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
025|
026| v = vector4(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
027| print()
028| exercise_vector(v)
029|
Using the @property decorator (lines #13, #16, and #19) makes the x, y, and z methods act like data properties rather than callable methods. When used as shown above, the properties only have a get method, so are read-only. Note that each method returns the appropriate sequence member.
When run, this prints:
v = [2.718, 3.142, 6.626] v.x = 2.71828 v.y = 3.14159 v.z = 6.62607 'vector4' object has no attribute 'w' can't set attribute 'x' v.a = 21 can't delete attribute 'z' 'vector4' object does not support item assignment 'vector4' object doesn't support item deletion
This is the simplest implementation. Python generates all the errors; the code doesn’t raise any exceptions on its own. The set and delete errors are because the property descriptors Python creates don’t have set or delete methods. As with vector3, attempts to modify the underlying tuple fail.
We can expand on vector4 by supplying the missing set and delete methods:
002|
003| class vector5 (tuple):
004| def __new__ (cls, x=0.0, y=0.0, z=0.0, **kwargs):
005| ”’New vector instance.”’
006| return super().__new__(cls, [x,y,z])
007|
008| def __init__ (self, *args, fmt=‘.2f’):
009| ”’Initialize vector instance.”’
010| super().__init__()
011| self._fmt = fmt
012|
013| def __getattr__ (self, name):
014| ”’No setting of attributes allowed.”’
015| if name in [‘x’,‘y’,‘z’,‘_fmt’]:
016| return super().__getattr__(name)
017| raise AttributeError(f’Unknown attribute “{name}”‘)
018|
019| def _getx (self): return self[0]
020| def _gety (self): return self[1]
021| def _getz (self): return self[2]
022|
023| def _setany (self, value):
024| raise NotImplementedError(‘Attributes cannot be modified.’)
025|
026| def _delany (self):
027| raise NotImplementedError(‘Attributes cannot be deleted.’)
028|
029| x = property(_getx, _setany, _delany, “X element”)
030| y = property(_gety, _setany, _delany, “Y element”)
031| z = property(_getz, _setany, _delany, “Z element”)
032|
033| def __repr__ (self):
034| f = self._fmt
035| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
036|
037| v = vector5(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
038| print()
039| exercise_vector(v)
040|
Since modifying a property always raises an exception, we can use generic _setany and _delany methods (lines #23 to #27) for all three properties. Using @property this way (lines #29 to #31) lets us control the exception and add doc strings to the properties.
We also implement the __getattr__ method (lines #13 to #17) to handle attempted access to properties that don’t exist. When run, this prints:
v = [2.718, 3.142, 6.626] v.x = 2.71828 v.y = 3.14159 v.z = 6.62607 Unknown attribute "w" Attributes cannot be modified. v.a = 21 Attributes cannot be deleted. 'vector5' object does not support item assignment 'vector5' object doesn't support item deletion
Essentially the same as vector4 but now we control the attribute error text. But in trying to access a property that doesn’t exist, note how we use the name of the property, w. We can’t do that for the set and delete errors because the generic functions don’t know which property is accessed. A minor detail, perhaps, but we can have it all.
To take this to the next level, we create our own descriptor class:
002|
003| class vector_element:
004| def __init__ (self):
005| print(f’element::init()’)
006| self.ix = –1
007|
008| def __set_name__ (self, cls, name):
009| print(f’setname({name}, cls={cls.__name__ if cls else “None”})’)
010| self.name = name
011| self.ix = [‘x’,‘y’,‘z’,‘w’,‘u’].index(name)
012|
013| def __get__ (self, obj, cls):
014| print(f’get({id(obj) if obj else “None”})’)
015| if obj:
016| return obj[self.ix]
017| return self.name
018|
019| def __set__ (self, obj, val):
020| print(f’set({id(obj)}, {val})’)
021| raise AttributeError(f’Vector element “{self.name}” is read-only!’)
022|
023| def __delete__ (self, obj):
024| print(f’del({id(obj)})’)
025| raise AttributeError(f’Vector element “{self.name}” cannot be deleted!’)
026|
027| class vector6 (tuple):
028| def __new__ (cls, x=0.0, y=0.0, z=0.0, **kwargs):
029| ”’New vector instance.”’
030| return super().__new__(cls, [x,y,z])
031|
032| def __init__ (self, *args, fmt=‘.2f’):
033| ”’Initialize vector instance.”’
034| super().__init__()
035| self._fmt = fmt
036|
037| def __getattr__ (self, name):
038| ”’No setting of attributes allowed.”’
039| if name in [‘x’,‘y’,‘z’,‘_fmt’]:
040| return super().__getattr__(name)
041| raise AttributeError(f’Unknown attribute “{name}”‘)
042|
043| # Vector elements X, Y, and Z…
044| x = vector_element()
045| y = vector_element()
046| z = vector_element()
047|
048| def __repr__ (self):
049| f = self._fmt
050| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
051|
052| v = vector6(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
053| print()
054| exercise_vector(v)
055|
I’ve instrumented the vector_element descriptor (lines #3 to #25) with print statements so we can see how this interacts with vector6 (see Python Descriptors, part 1 and Python Descriptors, part 2 for more information on descriptors).
Of note is the __set_name__ method (lines #8 to #11), which tells the property instance what its name is. We store the name (line #10) for use in error messages and determine the sequence index value corresponding to the vector element represented (line #11).
The __get__ method uses the index to return the appropriate tuple element.
When run, this prints:
init() init() init() setname(x, cls=vector6) setname(y, cls=vector6) setname(z, cls=vector6) get(1424131852464) get(1424131852464) get(1424131852464) v = [2.718, 3.142, 6.626] get(1424131852464) v.x = 2.71828 get(1424131852464) v.y = 3.14159 get(1424131852464) v.z = 6.62607 Unknown attribute "w" set(1424131852464, 42) Vector element "x" is read-only! v.a = 21 del(1424131852464) Vector element "z" cannot be deleted! 'vector6' object does not support item assignment 'vector6' object doesn't support item deletion
The first six lines come from the creation of the vector6 class as the three properties are created. In the four lines after, the get method is called three times to obtain the three vector elements. The six lines below show the same thing, access to a vector element goes through the get method.
The set and delete methods raise appropriate errors (which now include the element name). And, as always, we can add and access new attributes. The only error messages we don’t control involve accessing the underlying tuple. Let’s tackle those next.
Our last version of a vector class completely manages access to its elements:
002| from examples import vector_element, exercise_vector
003|
004| class vector7 (tuple):
005| def __new__ (cls, x=0.0, y=0.0, z=0.0, **kwargs):
006| ”’New vector instance.”’
007| return super().__new__(cls, [x,y,z])
008|
009| def __init__ (self, *args, fmt=‘.2f’):
010| ”’Initialize vector instance.”’
011| super().__init__()
012| self._fmt = fmt
013|
014| # Vector elements X, Y, and Z…
015| x = vector_element()
016| y = vector_element()
017| z = vector_element()
018|
019| def __getattr__ (self, name):
020| ”’No setting of attributes allowed.”’
021| if name in [‘x’,‘y’,‘z’,‘_fmt’]:
022| return super().__getattr__(name)
023| raise AttributeError(f’Unknown attribute “{name}”‘)
024|
025| def __getitem__ (self, ix):
026| ”’Get an element by index.”’
027| if 0 <= ix < 3:
028| return super().__getitem__(ix)
029| raise AttributeError(f’Vector element vec[{ix}] does not exist!’)
030|
031| def __setitem__ (self, ix, value):
032| ”’Set an element by index.”’
033| raise AttributeError(f’Vector element vec[{ix}] is read-only!’)
034|
035| def __delitem__ (self, ix):
036| ”’Delete an element by index.”’
037| raise AttributeError(f’Vector element vec[{ix}] cannot be deleted!’)
038|
039| def __repr__ (self):
040| f = self._fmt
041| return f'[{self.x:{f}}, {self.y:{f}}, {self.z:{f}}]’
042|
043| v = vector7(2.71828, 3.14159, 6.62607, fmt=‘.3f’)
044| print()
045| exercise_vector(v)
046|
047| try:
048| print(f'{v[0] = }’)
049| print(f'{v[1] = }’)
050| print(f'{v[2] = }’)
051| print(f'{v[3] = }’)
052| except Exception as e:
053| print(e, file=stderr)
054| print()
055|
In addition to the properties (lines #15 to #17), which we defined for vector6, we also implement the indexing methods, __getitem__ (lines #25 to #29), __setitem__ (lines #31 to #33), and __delitem__ (lines #35 to #37). These methods come into play when we index an object as a sequence.
Note the additional exercise code designed to test indexing (lines #47 to #53).
When run, this prints:
v = [2.718, 3.142, 6.626] v.x = 2.71828 v.y = 3.14159 v.z = 6.62607 Unknown attribute "w" Vector element "x" is read-only! v.a = 21 Vector element "z" cannot be deleted! Vector element vec[0] is read-only! Vector element vec[1] cannot be deleted! v[0] = 2.71828 v[1] = 3.14159 v[2] = 6.62607 Vector element vec[3] does not exist!
Note how we handle all exceptions and that our error messages reference the element in question.
Hopefully this and the previous post give you a good idea how to make your own powerful classes as well as how to do it by subclassing existing Python classes.
The ZIP file linked below contains an expanded version of vector3d that includes everything from this post as well as the vector math methods from last time, so it’s a fully featured, fully managed class you can explore and use as a template for your own projects.
Link: Zip file containing all code fragments used in this post.
∅
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 #12