I'm working on a fun project where I'm trying to implement Random Forest algorithm in pure Python, i.e. without NumpyNumPy. But then I'll still be dealing with arrays all the time and so I'm writing my own Array class to use in place of Numpy'sNumPy's ndarray. Although I'm only focusing on 2d arrays as I don't need n-dimensional stuff for this project.
In the normal andditionaddition or multiplication of arrays in NumpyNumPy the result is element-wise addition or multiplication and my solution is O(n * m) which for large arrays will be a problem.
from typing import Iterable
from pathlib import Path
from functools import reduce
class Array:
def __init__(self, value: Iterable[Iterable[int | float]]) -> None:
self.value = value
@property
def value(self):
return self.__value
@value.setter
def value(self, val: Iterable[Iterable[int | float]]):
if not isinstance(val, Iterable):
raise ValueError('The array must be an Iterable.')
if not all(isinstance(a, Iterable) for a in val):
raise ValueError("All elements of array must be Iterables. "
"If creating a flat array, all elements must be length 1 iterables.")
if not reduce(lambda x, y: len(x)==len(y), val):
raise ValueError("Can not create array from a ragged sequence. "
"Please ensure all elements have the same length.")
self.__value = val
@property
def shape(self) -> tuple[int, int]:
return (len(self.value), len(self.value[0]))
def __repr__(self):
return f"Array({self.value}, shape={self.shape})"
def __eq__(self, other: 'Array') -> bool:
if self.shape != other.shape:
return False
return all(x == y for x, y in zip(self.value, other.value))
def __neq__(self):
return not self.__eq__
def __getitem__(self, idx: tuple[int, int]) -> int | float:
if idx[0] > self.shape[0] or idx[1] > self.shape[1]:
raise IndexError(f"Index out of bounds for the array of shape {self.shape}")
return self.value[idx[0]][idx[1]]
def __add__(self, other: 'Array') -> 'Array':
v = []
if self.shape != other.shape:
raise ValueError("Can only add arrays of the same shape.")
for i in range(self.shape[0]):
v.append([])
for j in range(self.shape[1]):
v[i].append(self.value[i][j] + other.value[i][j])
return Array(v)
def __mul__(self, other: 'Array') -> 'Array':
v = []
if self.shape != other.shape:
raise ValueError("Can only add arrays of the same shape.")
for i in range(self.shape[0]):
v.append([])
for j in range(self.shape[1]):
v[i].append(self.value[i][j] * other.value[i][j])
return Array(v)
@classmethod
def from_text(cls, input_file: str | Path, skip_rows: int | None = None, delimeter: str = ',') -> 'Array':
inp = Path(input_file)
if not input_file or not inp.exists():
raise IOError("No input file provided or file does not exist.")
with open(inp, 'r') as file:
arr = []
for idx, i in enumerate(file.readlines()):
if skip_rows is None or skip_rows <= idx:
try:
arr.append([float(k) for k in i.strip().split(delimeter)])
except (ValueError, TypeError) as e:
raise TypeError("Input data must be castable to float. "
"Found incompatible data type. Check your inputs.") from e
return cls(arr)
def matmul(self, other: 'Array') -> 'Array':
pass
```