Magic Methods
Problem Statement
Implement a Vector class with magic methods for arithmetic operations.
Background
Python magic methods enable operator overloading:
- add: + operator
- sub: - operator
- mul: * operator (scalar)
- eq: == operator
- len: len() function
Your Task
Create a Vector class that supports:
- Addition: v1 + v2 (element-wise)
- Subtraction: v1 - v2 (element-wise)
- Scalar multiplication: v * scalar
- Equality: v1 == v2
- Length: len(v) returns number of elements
- str: "Vector([...])"
Example:
Vector([1, 2, 3]) + Vector([4, 5, 6])
Vector([5, 7, 9])
Element-wise: 1+4=5, 2+5=7, 3+6=9
Constraints:
- Vectors for arithmetic must have same length
- Return new Vector objects, don't modify originals
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.