intro-to-python/11_classes/sample_package/vector.py

262 lines
8.1 KiB
Python

"""This module defines a Vector class."""
# Imports from the standard library go first ...
import numbers
# ... and are followed by project-internal ones.
# If third-party libraries are needed, they are
# put into a group on their own in between.
# Within a group, imports are sorted lexicographically.
from sample_package import matrix
from sample_package import utils
class Vector:
"""A one-dimensional vector from linear algebra.
All entries are converted to floats, or whatever is set in the typing attribute.
Attributes:
matrix_cls (matrix.Matrix): a reference to the Matrix class to work with
storage (callable): data type used to store the entries internally;
defaults to tuple
typing (callable): type casting applied to all entries upon creation;
defaults to float
zero_threshold (float): max. tolerance when comparing an entry to zero;
defaults to 1e-12
"""
matrix_cls = matrix.Matrix
storage = utils.DEFAULT_ENTRIES_STORAGE
typing = utils.DEFAULT_ENTRY_TYPE
zero_threshold = utils.ZERO_THRESHOLD
def __init__(self, data):
"""Create a new vector.
Args:
data (sequence): the vector's entries
Raises:
ValueError: if no entries are provided
Example Usage:
>>> Vector([1, 2, 3])
Vector((1.0, 2.0, 3.0))
>>> Vector(range(3))
Vector((0.0, 1.0, 2.0))
"""
self._entries = self.storage(self.typing(x) for x in data)
if len(self) == 0:
raise ValueError("a vector must have at least one entry")
def __repr__(self):
"""Text representation of a Vector."""
name = self.__class__.__name__
args = ", ".join(repr(x) for x in self)
return f"{name}(({args}))"
def __str__(self):
"""Human-readable text representation of a Vector."""
name = self.__class__.__name__
first, last, n_entries = self[0], self[-1], len(self)
return f"{name}({first!r}, ..., {last!r})[{n_entries:d}]"
def __len__(self):
"""Number of entries in a Vector."""
return len(self._entries)
def __getitem__(self, index):
"""Obtain an individual entry of a Vector."""
if not isinstance(index, int):
raise TypeError("index must be an integer")
return self._entries[index]
def __iter__(self):
"""Loop over a Vector's entries."""
return iter(self._entries)
def __reversed__(self):
"""Loop over a Vector's entries in reverse order."""
return reversed(self._entries)
def __add__(self, other):
"""Handle `self + other` and `other + self`.
This may be either vector addition or broadcasting addition.
Example Usage:
>>> Vector([1, 2, 3]) + Vector([2, 3, 4])
Vector((3.0, 5.0, 7.0))
>>> Vector([1, 2, 3]) + 4
Vector((5.0, 6.0, 7.0))
>>> 10 + Vector([1, 2, 3])
Vector((11.0, 12.0, 13.0))
"""
# Vector addition
if isinstance(other, self.__class__):
if len(self) != len(other):
raise ValueError("vectors must be of the same length")
return self.__class__(x + y for (x, y) in zip(self, other))
# Broadcasting addition
elif isinstance(other, numbers.Number):
return self.__class__(x + other for x in self)
return NotImplemented
def __radd__(self, other):
"""See docstring for .__add__()."""
# As both vector and broadcasting addition are commutative,
# we dispatch to .__add__().
return self + other
def __sub__(self, other):
"""Handle `self - other` and `other - self`.
This may be either vector subtraction or broadcasting subtraction.
Example Usage:
>>> Vector([7, 8, 9]) - Vector([1, 2, 3])
Vector((6.0, 6.0, 6.0))
>>> Vector([1, 2, 3]) - 1
Vector((0.0, 1.0, 2.0))
>>> 10 - Vector([1, 2, 3])
Vector((9.0, 8.0, 7.0))
"""
# As subtraction is the inverse of addition,
# we first dispatch to .__neg__() to invert the signs of
# all entries in other and then dispatch to .__add__().
return self + (-other)
def __rsub__(self, other):
"""See docstring for .__sub__()."""
# Same comments as in .__sub__() apply
# with the roles of self and other swapped.
return (-self) + other
def __mul__(self, other):
"""Handle `self * other` and `other * self`.
This may be either the dot product of two vectors or scalar multiplication.
Example Usage:
>>> Vector([1, 2, 3]) * Vector([2, 3, 4])
20.0
>>> 2 * Vector([1, 2, 3])
Vector((2.0, 4.0, 6.0))
>>> Vector([1, 2, 3]) * 3
Vector((3.0, 6.0, 9.0))
"""
# Dot product
if isinstance(other, self.__class__):
if len(self) != len(other):
raise ValueError("vectors must be of the same length")
return sum(x * y for (x, y) in zip(self, other))
# Scalar multiplication
elif isinstance(other, numbers.Number):
return self.__class__(x * other for x in self)
return NotImplemented
def __rmul__(self, other):
"""See docstring for .__mul__()."""
# As both dot product and scalar multiplication are commutative,
# we dispatch to .__mul__().
return self * other
def __truediv__(self, other):
"""Handle `self / other`.
Divide a Vector by a scalar.
Example Usage:
>>> Vector([9, 6, 12]) / 3
Vector((3.0, 2.0, 4.0))
"""
# As scalar division division is the same as multiplication
# with the inverse, we dispatch to .__mul__().
if isinstance(other, numbers.Number):
return self * (1 / other)
return NotImplemented
def __eq__(self, other):
"""Handle `self == other`.
Compare two Vectors for equality.
Example Usage:
>>> Vector([1, 2, 3]) == Vector([1, 2, 3])
True
>>> Vector([1, 2, 3]) == Vector([4, 5, 6])
False
"""
if isinstance(other, self.__class__):
if len(self) != len(other):
raise ValueError("vectors must be of the same length")
for x, y in zip(self, other):
if abs(x - y) > self.zero_threshold:
return False # exit early if two corresponding entries differ
return True
return NotImplemented
def __pos__(self):
"""Handle `+self`.
This is simply an identity operator returning the Vector itself.
"""
return self
def __neg__(self):
"""Handle `-self`.
Negate all entries of a Vector.
"""
return self.__class__(-x for x in self)
def __abs__(self):
"""The Euclidean norm of a vector."""
return utils.norm(self) # uses the norm() function shared matrix.Matrix
def __bool__(self):
"""A Vector is truthy if its Euclidean norm is strictly positive."""
return bool(abs(self))
def __float__(self):
"""Cast a Vector as a scalar.
Returns:
scalar (float)
Raises:
RuntimeError: if the Vector has more than one entry
"""
if len(self) != 1:
raise RuntimeError("vector must have exactly one entry to become a scalar")
return self[0]
def as_matrix(self, *, column=True):
"""Get a Matrix representation of a Vector.
Args:
column (bool): if the vector is interpreted as a
column vector or a row vector; defaults to True
Returns:
matrix (matrix.Matrix)
Example Usage:
>>> v = Vector([1, 2, 3])
>>> v.as_matrix()
Matrix(((1.0,), (2.0,), (3.0,)))
>>> v.as_matrix(column=False)
Matrix(((1.0, 2.0, 3.0,)))
"""
if column:
return self.matrix_cls([x] for x in self)
return self.matrix_cls([(x for x in self)])