intro-to-python/sample_package/matrix.py
Alexander Hess ec3097e220 Add the sample_package folder to the project
The sample_package folder serves as an example in notebook 02 as to what
a Python package looks like. Its contents actually belong to the chapter
on object-orientation.
2019-09-22 20:26:58 +02:00

254 lines
8.6 KiB
Python

"""This module defines a Matrix class."""
class Matrix:
"""A standard m-by-n-dimensional matrix from linear algebra.
The class is designed for sub-classing in such a way that
the user can adapt the typing class attribute to change,
for example, how the entries are stored (e.g., as integers).
Attributes:
storage (callable): must return an iterable that is used
to store the entries of the matrix; defaults to tuple
typing (callable): type casting applied to all vector
entries upon creation; defaults to float
zero_threshold (float): maximum difference allowed when
comparing an entry to zero; defaults to 1e-12
"""
storage = tuple
typing = float
zero_threshold = 1e-12
def __init__(self, data):
"""Initiate a new matrix.
Args:
data (iterable of iterables): the matrix's entries;
must be provided with rows first, then column;
the number of column entries must be consistent across rows
where the first row sets the standard;
must have at least one element in total
Raises:
ValueError:
- if the number of columns is inconsistent across the rows
- if the provided data do not have enough entries
"""
self._entries = self.storage(
self.storage(self.typing(x) for x in r) for r in data
)
for row in self._entries[1:]:
if len(row) != self.n_cols:
raise ValueError("each row must have the same number of entries")
if len(self) == 0:
raise ValueError("the matrix must have at least one entry")
@classmethod
def from_columns(cls, data):
"""Initiate a new matrix.
This is an alternative constructor for data provided in column-major order.
Args:
data (iterable of iterables): the matrix's entries in column-major order;
the number of column entries must be consistent per row
while the first row sets the correct number;
must have at least one element in total
Raises:
ValueError:
- if the number of columns is inconsistent across the rows
- if the provided data do not have enough entries
"""
return cls(data).transpose()
def __repr__(self):
name = self.__class__.__name__
args = ", ".join(
"(" + ", ".join(f"{c:.3f}" for c in r) + ",)" for r in self._entries
)
return f"{name}(({args}))"
def __str__(self):
name = self.__class__.__name__
first, last, m, n = self[0], self[-1], self.n_rows, self.n_cols
return f"{name}(({first:.1f}, ...), ..., (..., {last:.1f}))[{m:d}x{n:d}]"
@property
def n_rows(self):
"""Number of rows in the matrix."""
return len(self._entries)
@property
def n_cols(self):
"""Number of columns in the matrix."""
return len(self._entries[0])
def __len__(self):
return self.n_rows * self.n_cols
def __getitem__(self, index):
if isinstance(index, int):
if index < 0:
index += len(self)
if not (0 <= index < len(self)):
raise IndexError("integer index out of range")
row, col = divmod(index, self.n_cols)
return self._entries[row][col]
elif (
isinstance(index, tuple)
and len(index) == 2
and isinstance(index[0], int)
and isinstance(index[1], int)
):
return self._entries[index[0]][index[1]]
raise TypeError("index must be either an integer or a tuple of two integers")
def rows(self):
"""Iterate over the rows of the matrix.
Returns:
rows (Generator): produces Vector instances
representing individual rows of the matrix
"""
return (Vector(r) for r in self._entries)
def cols(self):
"""Iterate over the columns of the matrix.
Returns:
columns (Generator): produces Vector instances
representing individual columns of the matrix
"""
return (
Vector(self._entries[r][c] for r in range(self.n_rows))
for c in range(self.n_cols)
)
def entries(self, *, reverse=False, row_major=True):
"""Iterate over the entries of the matrix in flat fashion.
Args:
reverse (bool): flag to iterate backwards; defaults to False
row_major (bool): flag to iterate in row major order; defaults to False
Returns:
entries (Generator): produces the entries rows of the matrix
in the type set in the typing class variable
"""
if reverse:
rows, cols = range(self.n_rows - 1, -1, -1), range(self.n_cols - 1, -1, -1)
else:
rows, cols = range(self.n_rows), range(self.n_cols)
if row_major:
return (self._entries[r][c] for r in rows for c in cols)
return (self._entries[r][c] for c in cols for r in rows)
def __iter__(self):
return self.entries()
def __reversed__(self):
return self.entries(reverse=True)
def __add__(self, other):
if isinstance(other, self.__class__):
if (self.n_rows != other.n_rows) or (self.n_cols != other.n_cols):
raise ValueError("matrices need to be of the same dimensions")
return self.__class__(
(s_col + o_col for (s_col, o_col) in zip(s_row, o_row))
for (s_row, o_row) in zip(self._entries, other._entries)
)
elif isinstance(other, numbers.Number):
return self.__class__((c + other for c in r) for r in self._entries)
return NotImplemented
def __radd__(self, other):
if isinstance(other, Vector):
raise TypeError("vectors and matrices cannot be added")
return self + other
def __sub__(self, other):
return self + (-other)
def __rsub__(self, other):
if isinstance(other, Vector):
raise TypeError("vectors and matrices cannot be subtracted")
return (-self) + other
def _matrix_multiply(self, other):
if self.n_cols != other.n_rows:
raise ValueError("matrices need to have compatible dimensions")
return self.__class__((rv * cv for cv in other.cols()) for rv in self.rows())
def __mul__(self, other):
if isinstance(other, numbers.Number):
return self.__class__((x * other for x in r) for r in self._entries)
elif isinstance(other, Vector):
return self._matrix_multiply(other.as_matrix()).as_vector()
elif isinstance(other, self.__class__):
return self._matrix_multiply(other)
return NotImplemented
def __rmul__(self, other):
if isinstance(other, numbers.Number):
return self * other
elif isinstance(other, Vector):
return other.as_matrix(column=False)._matrix_multiply(self).as_vector()
return NotImplemented
def __truediv__(self, other):
if isinstance(other, numbers.Number):
return self * (1 / other)
return NotImplemented
def __eq__(self, other):
if isinstance(other, self.__class__):
if (self.n_rows != other.n_rows) or (self.n_cols != other.n_cols):
raise ValueError("matrices need to be of the same dimensions")
for x, y in zip(self, other):
if abs(x - y) > self.zero_threshold:
return False
return True
return NotImplemented
def __pos__(self):
return self
def __neg__(self):
return self.__class__((-x for x in r) for r in self._entries)
def __abs__(self):
return norm(self)
def __bool__(self):
return bool(abs(self))
def __float__(self):
if not (self.n_rows == 1 and self.n_cols == 1):
raise RuntimeError("matrix must have exactly one entry to become a scalar")
return self[0]
def as_vector(self):
"""Cast the matrix as a one-dimensional vector.
Returns:
vector (Vector)
Raises:
RuntimeError: if not one of the two dimensions is 1
"""
if not (self.n_rows == 1 or self.n_cols == 1):
raise RuntimeError("one dimension (m or n) must be 1")
return Vector(x for x in self)
def transpose(self):
"""Transpose the rows and columns of the matrix.
Returns:
matrix (Matrix)
"""
return self.__class__(zip(*self._entries))
from .vector import Vector