I'm working on a fun project where I'm trying to implement Random Forest algorithm in pure Python, i.e. without NumPy. 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's ndarray. Although I'm only focusing on 2d arrays as I don't need n-dimensional stuff for this project.
It's all working correctly, although it's not fully covered by tests yet. And for most part I think I have reasonably decent performance, but I'm struggling with methods for addition and multiplication of arrays.
In the normal addition or multiplication of arrays in NumPy the result is element-wise addition or multiplication and my solution is O(n * m) which for large arrays will be a problem.
Is there a faster algorithm to do this (the __add__
and __mul__
dunders)? Any other things I could improve?
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
__mul__()
looks copy-pasted from__add__()
- a "code smell". (Look at the C&P error…). With n * m result values, what time complexity are you hoping for? \$\endgroup\$__add__
because I couldn't come up with a different solution and figured it should follow the same principle. I'm not looking for any specifc time complexity, just trying to see if there are algorithms that can do it faster than what I have. But it's not really critical, as long as it works. \$\endgroup\$