Sequences and Special Methods
Source: Fluent Python, 2nd Edition — Chapter 12 by Luciano Ramalho
A 2-D vector was a pair of named floats. An n-D vector is a flat sequence of floats: same dunders as tuple or array where they make sense, stored by composition (an array inside), not by inheriting list.
Search engines treat documents as vectors with one axis per word. Cosine similarity is real work for NumPy, SciPy, or gensim. This chapter is about the data model for a sequence type, not production linear algebra.
The constructor takes one iterable, like every built-in sequence: Vector([3, 4, 5]), Vector(range(10)). Vector(3, 4) would fight that convention. Two-component results can still match the old Vector2d tests; the call shape does not.
Take #1: compatible, but repr must stay small
Keep _components as array('d', ...). Iterate it. str can stay str(tuple(self)). bytes / frombytes can dump the array (pass a memoryview into the constructor; do not unpack with *). math.hypot(*self) works in n-D from Python 3.8.
repr is for debugging. A thousand-dimension object must not print a thousand lines. reprlib.repr cuts the display and inserts .... Apply it to the array, then keep only the [...] part so the user sees Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...]) instead of array('d', ...). Copying to a list just to reuse list-repr wastes memory.
repr() itself should not raise. If formatting fails, still emit something that identifies the object.
Subclassing Vector2d is a poor fit: constructors disagree, and the point of this type is to be a sequence, not a 2-D subclass.
🧪 reprlib on a long array
Sequence constructor
1 / 1A sequence-like Vector should take
Pick an answer to see if you are right.
Protocols and duck typing
A protocol here is an informal interface: documented by behavior, not by a base class. The sequence protocol is __len__ and __getitem__ with the usual meanings. FrenchDeck is a sequence because it acts like one. Alex Martelli’s line is the slogan: do not ask “is-a duck”; ask whether it quacks the way you need.
You can implement part of a protocol for a known context. Iteration historically works from __getitem__ alone; many call sites still want __len__.
PEP 544 static protocols (typing.Protocol) are related but stricter: you must provide every method the protocol class lists. Dynamic duck typing stays the older, looser idea.
Take #2: __len__, __getitem__, and real slices
Delegating both methods to _components is a one-liner each. Indexing then works. Slicing “works” too — and returns an array, which drops your type. Built-in sequences return the same type.
Python turns seq[1:4:2] into seq.__getitem__(slice(1, 4, 2)). Commas inside [] pass a tuple (possibly of slices). slice has start, stop, step, and indices(len) which normalizes negatives and oversize bounds the way 'ABCDE'[:10:2] does. If you already hold an array, you can slice that array and wrap the result; indices is the lifesaver when you do not.
A slice-aware __getitem__:
def __getitem__(self, key):
if isinstance(key, slice):
cls = type(self)
return cls(self._components[key])
index = operator.index(key)
return self._components[index]
isinstance on slice is a justified special case. operator.index (PEP 357) is for “this value is an index,” not for “coerce like int.” int(3.14) is 3; operator.index(3.14) is a TypeError. A tuple key fails the same way: this vector is not a matrix.
Take #3: v.x without four properties
Shortcuts x, y, z, t for the first four components beat writing four @propertys. __getattr__ runs only after normal lookup fails. Map the name through __match_args__ = ('x', 'y', 'z', 't') and index _components if the position exists; otherwise raise AttributeError. A 3-D vector has no .t.
That is not enough. v.x = 10 creates a real instance attribute. Later reads skip __getattr__ and return 10 while [0] is still 0.0. Pair __getattr__ with __setattr__: refuse single-letter names in __match_args__ (read-only) and other lowercase single letters (too easy to confuse with axes). Delegate everything else with super().__setattr__.
Do not use __slots__ just to block extra attributes. Slots are a memory trade; they have inheritance caveats.
Keep the vector immutable if you plan to hash it. __setitem__ can wait.
The last test (k get) is missing from __getattr__. After a successful set on x without __setattr__ guards, get would show 9 while the list still starts with 0 — that is the bug you are closing. blocked 0.0 means the virtual axis still reads from _xs.
Take #4: hash with reduce, equality with zip
Two-D hash was hash(x) ^ hash(y). n-D is XOR of every component hash. functools.reduce folds a two-arg function across an iterable. operator.xor beats a lambda. Always pass an initializer: 0 for ^ (and for +, |); 1 for * and &. Empty input without it raises TypeError.
That is map-reduce: map(hash, components) then XOR. In Python 3 map is lazy, like a generator expression.
tuple(self) == tuple(other) copies huge vectors. Prefer:
def __eq__(self, other):
return len(self) == len(other) and all(
a == b for a, b in zip(self, other)
)
zip stops at the shortest input, so the length check is required. Python 3.10 added zip(..., strict=True) to fail when lengths differ. itertools.zip_longest pads instead. zip(*rows) transposes nested sequences. enumerate is the usual partner when you still need an index.
Vector([1, 2]) == (1, 2) stays true with this __eq__; live with it until operator overloading.
Empty sequences hash to the XOR identity 0 only if you pass that initializer. The length-trap row is False even though a naive zip would compare 1,2 and stop.
Take #5: format in n-D (and a peek at h)
Cartesian __format__ applies the spec to each component and joins with ', ' inside '({})'. For a custom suffix, Vector2d used p (polar). n-D uses h (hyperspherical): magnitude plus n−1 angles, angle brackets. The angles come from n-sphere formulas (angle / angles); itertools.chain glues abs(self) onto the angle stream. Avoid colliding with float codes eEfFgGn%.
The spherical math is a lookup in the n-sphere article, not the point of the data model. The exams stay on Cartesian join and the h wrapper idea: suffix h means brackets around formatted coords you already know how to list.
That h path is the shape of hyperspherical display (angle brackets, remaining spec on each number). Real Vector also replaces the components with radius and angles before formatting.
Chapter 16 will add infix operators. Generators inside angles / chain return in the generators chapter.