1. Background Knowledge
Magic methods (also called dunder methods, from "double underscore") are special methods in Python classes that allow operator overloading, enabling custom objects to behave like built-in types. For example, defining add(self, other) makes instances support the + operator, while len(self) works with len(). This leverages Python's object-oriented design, where classes can customize behavior for operators, comparisons, and built-in functions without explicit method calls.
In the context of a Vector class, these methods enable intuitive arithmetic like v1 + v2 for element-wise addition, mimicking NumPy arrays. The class typically stores a list of numbers internally. Key theory involves operator precedence and associativity (e.g., + and - are left-associative) and type consistency—operations should return new Vector instances, not modify originals (immutability principle). Understanding self vs. other parameters is crucial, as other might not be a Vector (e.g., scalar multiplication).
2. Algorithm/Approach
The general pattern is defensive operator overloading:
- Store data as a private list (e.g., self._components).
- Implement each magic method to:
- Validate inputs (ensure compatible lengths/types).
- Perform element-wise operations using list comprehensions or map.
- Return a new Vector instance.
- For scalar *, detect if other is numeric (use isinstance(other, (int, float))).
- Use str for readable output and eq for deep equality (compare components).
This mirrors functional programming paradigms: pure operations creating new objects without side effects.
3. Step-by-Step Strategy
-
Initialize the class: In init(self, components), validate input as iterable of numbers, store as self._components = list(components).
-
Implement arithmetic (add, sub, mul):
- Check len(self._components) == len(other._components) for vector ops.
- For scalar mul, swap roles if needed (implement rmul for scalar * v).
- Compute new list: [self._components[i] op other._components[i] for i in range(len(self))].
- Return Vector(new_list).
- Add utility methods:
- len(self): Return len(self._components).
- eq(self, other): Check type match and self._components == other._components.
- str(self): Return f"Vector({self._components})".
-
Test edge cases: Empty vectors, mismatched lengths (raise ValueError), scalars vs. vectors.
-
Polish: Ensure operations chain (e.g., (v1 + v2) * 2).
4. Common Pitfalls
- Mutating self: Always return new Vector; don't modify self._components.
- Type mismatches: v1 + 5 or 5 + v1 fails without proper checks/radd/rmul.
- Length errors: No auto-broadcasting—explicitly raise errors for unequal lengths.
- Shallow copies: Use list(components) in init to avoid external list references.
- eq pitfalls: Compare contents, not object identity (is); handle None.
- Infinite recursion: Avoid calling other magic methods inside themselves.
- Floating-point equality: For eq, exact match is fine for integers; consider tolerance for floats.
5. Time & Space Complexity
- Arithmetic ops (add, etc.): O(n) time and space, where n= vector length (element-wise iteration + new list creation).
- eq: O(n) time (list comparison).
- len, str: O(1) time, O(n) for string (but negligible).
- Overall: Linear in vector size, efficient for medium-length vectors. No asymptotic issues for this problem.