7
\$\begingroup\$

it's the second object oriented program i've worked on, more details on https://hyperskill.org/projects/96?goal=391.

I've added as much documentation as needed, as a first time, to explain each component.

class Matrix:
 def __init__(self, dimension, elements=None):
 """
 Use:
 Matrix([num_of_rows, num_of_columns], [2D array or 1D array or None/blank])
 
 self.dimension is a list with two values, refering to the number of rows and columns of the matrix, Ex: [3, 3] is a 3x3 matrix
 self.matrix refers to the matrix in terms of 2D Arrays, 
 if elements is not given as an argument, then it will create a matrix of 0s with the dimensions provided
 if elements is given as a certain list/matrix, it will store it as a matrix in it
 self.transposition_type is a dictionary storing the possible transpositions functions of a matrix, if asked for
 Input Types:
 Matrix([3, 3]) or Matrix([3, 3], []) : creates a 3x3 matrix with only 0s
 Matrix([3, 3], [[1, 2, 3], [4, 5, 6], [7, 8, 9]]): creates a 3x3 matrix with a 2D array as elements
 Matrix([3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]) : creates a 3x3 matrix with a 1D array, elements are filled left to right, top to bottom in matrix
 
 """
 self.dimension = dimension
 self.transposition_type = {'main_diagonal': self.transposition_along_main_diagonal, 
 'side_diagonal': self.transposition_along_side_diagonal,
 'horizontal' : self.transposition_along_horizontal ,
 'vertical' : self.transposition_along_vertical } 
 self.matrix = self.default_matrix() if elements == None else self.check_elements(elements)
 def __add__(self, other):
 """
 Add two matrices
 Matrix + Matrix
 Rules:
 1) Matrixes should have the same dimensions
 2) Matrixes cannot be added by other data types
 """
 assert type(other) == Matrix, f"Matrix cannot be added by {type(other)}"
 assert self.dimension == other.dimension, "Dimensions should be same"
 return Matrix(self.dimension, [ [self.matrix[row][column] + other.matrix[row][column] for column in range(self.dimension[1])] for row in range(self.dimension[0])])
 def __sub__(self, other):
 """
 Subtract two matrices
 Matrix - Matrix
 Rules:
 1) Matrixes should have the same dimensions
 2) Matrixes cannot be added by other data types
 """
 assert type(other) == Matrix, f"Matrix cannot be subtracted by {type(other)}"
 assert self.dimension == other.dimension, "Dimensions should be same"
 return Matrix(self.dimension, [ [self.matrix[row][column] - other.matrix[row][column] for column in range(self.dimension[1])] for row in range(self.dimension[0])])
 def __mul__(self, other):
 """
 multiples a matrix with an integer/float or another matrix
 Multiplication process is different for a constant and another matrixes, so they go to different processes, depending on the type of other
 Matrix * constant | constant * Matrix
 Matrix * Matrix
 """
 if isinstance(other, (int, float)): # int/float * matrix
 return self.constant_multiplication(other)
 elif isinstance(other, Matrix):
 return self.matrix_multiplication(other)
 raise Exception(f"Matrixes cannot be multiplied by {type(other)}")
 def __rmul__(self, other):
 """
 Does the same thing as the __mul__ method, just instead of matrix * number, it also supports number * matrix
 """
 return self.__mul__(other)
 
 def __str__(self):
 """
 Displays Matrixes in a format
 Ex:
 Matrix( [3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]) or Matrix( [3, 3], [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
 1 2 3
 4 5 6
 7 8 9
 """
 return "\n".join(" ".join(str(value) for value in row) for row in self.matrix)
 def check_elements(self, elements):
 """
 Checks if the elements provided on instantiation is a 2D array or a 1D array, or None of them
 if its a 1D array, it calls the method self.set_matrix_elements_by_array(elements)
 if its a 2D array, it can be directly set to the self.matrix attribute directly
 if its an empty list [], then it will make a default matrix with 0s only
 """
 if elements:
 if all(map(lambda x: type(x) == list, elements)):
 if all(map(lambda x: type(x) == float or type(x) == int, elements[0])):
 return elements 
 elif all(map(lambda x: type(x) == float or type(x) == int, elements)):
 return self.set_matrix_elements_by_array(elements)
 raise Exception('Invalid Input Type')
 else:
 return self.default_matrix()
 
 def default_matrix(self, dimensions=None):
 """
 Makes a 0 element only matrix with the dimensions provided
 Dimensions cant be (0, 0)
 """
 dimension = self.dimension if dimensions == None else dimensions
 assert dimension[0] != 0 and dimension[1] != 0, "Dimensions cannot be (0, 0)"
 return [dimension[1] * [0] for _ in range(dimension[0])]
 def set_matrix_elements_by_array(self, elements): 
 """
 creates and returns a matrix (2D array) using a 1D array, given dimensions
 where the number of elements in the array must be equal to the product of the number of rows and columns
 """
 assert len(elements) == (self.dimension[0] * self.dimension[1]), "Number of elements is not equal"
 j = 0
 matrix = []
 for i in range(self.dimension[1], self.dimension[1]*self.dimension[0]+1, self.dimension[1]):
 matrix.append(elements[j:i])
 j = i
 return matrix
 def constant_multiplication(self, constant, matrix=None):
 """
 multiples a matrix with a constant and returns a new matrix
 """
 matrix = self.matrix if matrix is None else matrix
 return Matrix(self.dimension, [ [round(matrix[row][column] * constant, 2) for column in range(self.dimension[1])] for row in range(self.dimension[0])])
 def matrix_multiplication(self, other):
 """
 multiples a matrix with another matrix and returns a new matrix
 creates a 0 element only 2D array with the appropriate dimensions, depending on the two matrixes multiplied
 then changes the 2D array in place, and creates and returns a matrix using that 2D array
 """
 assert self.dimension[1] == other.dimension[0], "The number of columns of the first matrix must equal the number of rows of the second matrix" 
 matrix_array = self.default_matrix([self.dimension[0], other.dimension[1]])
 for i in range(self.dimension[0]):
 for j in range(other.dimension[1]):
 for k in range(other.dimension[0]):
 matrix_array[i][j] += self.matrix[i][k] * other.matrix[k][j]
 return Matrix([self.dimension[0], other.dimension[1]], matrix_array)
 def transposition_along_main_diagonal(self, matrix=None):
 """
 performs transposition along the main diagonal from left to right
 - just switch the position of row and columns for each elements: element[row][column] = element[column][row] 
 Ex:
 the diagonal is represented by 1 5 6
 1 1 1 1 2 3 
 2 2 2 ---> 1 2 3
 3 3 3 1 2 3
 """
 matrix = self.matrix if matrix is None else matrix
 return list(map(list, zip(*matrix)))
 
 def transposition_along_side_diagonal(self):
 """
 performs transposition along the side diagonal from right to left
 - just perform a transposition along the main diagonal, then reverse the position of each row, and then reverse the elements in each row
 Ex:
 the diagonal is represented by -1 2 3
 1 1 -1 transpos 1 2 3 reverse_pos -1 -2 -3 reverse_rows -3 -2 -1
 2 2 -2 ---------> 1 2 3 ---------> 1 2 3 ------------> 3 2 1
 3 3 -3 -1 -2 -3 1 2 3 3 2 1
 """
 return [row[::-1] for row in self.transposition_along_main_diagonal()[::-1]]
 def transposition_along_horizontal(self):
 """
 performs transposition along the horizontal
 - just simply reverse the position of rows
 Ex:
 the horizontal is represented by 4 5 6
 1 2 3 reverse 7 8 9
 4 5 6 --------> 4 5 6
 7 8 9 1 2 3
 """ 
 return [row for row in self.matrix[::-1]]
 def transposition_along_vertical(self):
 """
 performs transposition along the vertical
 - just simply reverse the elements of each row
 Ex:
 the vertical is represented by 2 5 8
 1 2 3 rev elems 3 2 1
 4 5 6 ---------> 6 5 4 
 7 8 9 9 8 7
 """
 return [row[::-1] for row in self.matrix]
 def matrix_transposition(self, choice):
 """
 returns the transposition of a matrix as a Matrix object, using switch case like dictionaries, with the dimensions swapped
 """
 return Matrix([self.dimension[1], self.dimension[0]], self.transposition_type[choice]())
 def get_minor(self, matrix, i, j):
 """
 acquires the minor/submatrix of a matrix, with dimensions (n-1, n-1), with n being the current dimensions of the matrix, based on the ith row and jth column given, which is submatrix formed by all the other elements that dont have the row i and column j
 Ex:
 the minor of element (5) at i=1 j=1, cancels 2 and 8, because they are in the jth column and cancels 4 and 6 because they are in the ith row
 | 1 2 3 | | 1 3 |
 | 4 5 6 | ---> | 7 9 |
 | 7 8 9 | 
 """
 return [row[:j] + row[j+1:] for row in (matrix[:i]+matrix[i+1:])]
 def determinant_helper(self, matrix):
 """
 recursively finds the determinant of a matrix, given as an argument, by finding the minor of every matrix using cofactors till it reaches the base cases
 basecase 1: matrix 1x1, determinant is the element left in the matrix 
 Ex:
 | 17 | has a determinant of 17
 basecase 2: matrix 2x2, determinant is the difference between the product of diagonals
 Ex:
 | a b | 
 | c d | has a determinant of a*d-b*c
 """
 # base case for a 2x2 matrix
 if len(matrix) == 2:
 return (matrix[0][0]*matrix[1][1]-matrix[0][1]*matrix[1][0])*1.0
 # base case for a 1x1 matrix
 if len(matrix) == 1 and len(matrix[0]) == 1:
 return matrix[0][0]*1.0
 determinant = 0
 for c in range(len(matrix)):
 determinant += ((-1.0)**c) * matrix[0][c] * self.determinant_helper(self.get_minor(matrix, 0, c))
 return determinant
 def determinant(self):
 """
 finds the determinant by using the helper function to supply the current matrix of the object
 changes integer floats to integers, and so on
 """
 det = self.determinant_helper(self.matrix)
 return int(det) if det.is_integer() else det
 def inverse(self):
 """
 acquires the inverse form of a matrix by using laplace's expansion
 which is A^-1 (inverse matrix) = 1/determinant * C^T (transposed matrix of all cofactors of all elements in matrix along main diagonal)
 """
 determinant = self.determinant()
 matrix = self.matrix
 #assert determinant != 0, "Matrix does not have an inverse form"
 
 # base case, for 2x2 matrix
 if len(matrix) == 2:
 return Matrix(self.dimension, [ [matrix[1][1]/determinant, -1*matrix[0][1]/determinant], 
 [-1*matrix[1][0]/determinant, matrix[0][0]/determinant] ])
 # find matrix of cofactors
 cofactors = []
 for row in range(len(matrix)):
 cofactor_row = []
 for column in range(len(matrix)):
 minor = self.get_minor(matrix, row, column)
 cofactor_row.append( round(((-1)**(row+column)) * self.determinant_helper(minor) / determinant, 2) )
 cofactors.append(cofactor_row)
 cofactors = self.transposition_along_main_diagonal(cofactors)
 return Matrix(self.dimension, cofactors)
class MatrixCalculator:
 def __init__(self):
 """
 self.matrices : holds a list of matrices for operations to be performed on.
 self.count : represents the current prompt number to be displayed for an operation, and determines what is shown next, acts as an index in a list for self.prompts
 self.choices : holds the possible operations for the menu.
 self.prompts : holds the appropriate prompts for depending on whether 1 or 2 matrices should be inputted, and they are accessed using self.count, which is reset after each operation to 0.
 self.transposition_choice : holds the possible transpositions for the menu in transpositions, represents which user input, will call what type of transposition
 self.main : starts the program
 """
 self.matrices = []
 self.count = 0
 self.choices = {1: self.addition, 2: self.constant_multiplication, 3: self.matrix_multiplication, 4: self.transpose_matrix, 5: self.get_determinant, 6: self.get_inverse, 0: exit}
 self.prompts = {1: ['Enter size of matrix: ', 'Enter matrix: '],
 2: ['Enter size of first matrix: ', 'Enter size of second matrix: ', 'Enter first matrix: ', 'Enter second matrix: ']}
 self.transposition_choice = {1: 'main_diagonal', 2: 'side_diagonal', 3: 'vertical', 4: 'horizontal'}
 self.main()
 def clear_matrices_and_count(self):
 """
 clears the matrices in memory and resets the prompt count after each operation
 """
 self.matrices = []
 self.count = 0
 def display_choices_and_input(self):
 """
 displays the menu, and the possible operations, and asks for a response to which operation to perform
 self.choices key is located in __init__
 """
 print("1. Add matrices\n2. Multiply matrix by a constant\n3. Multiply matrices\n4. Transpose matrix\n5. Calculate a determinant\n6. Inverse matrix\n0. Exit")
 self.choices[int(input('Your choice: '))]()
 def input_matrix(self, matrix, n):
 """
 inputs a matrix using a specific format in command line, and the appropriate response based on the operation
 self.prompts refers to the prompts performed based on n which refers to the number of matrices to be inputted
 self.count refers to the prompt in this iteration of the program
 Ex:
 Enter Matrix:
 > 1 2 3
 > 4 5 6
 > 7 8 9
 """
 print(self.prompts[n][n + self.count])
 for row in range(len(matrix.matrix)):
 inp = input().split()
 try:
 matrix.matrix[row] = list(map(int, inp))
 except:
 matrix.matrix[row] = list(map(float, inp))
 self.count += 1
 def input_matrix_n_times(self, n):
 """
 allows the inputting of a matrice multiple times with a value of n which refer to the number of matrices to be inputted
 n refers to the number of matrices to be inputted and acts as a constant to output specific prompts for each operation
 """
 for i in range(n):
 self.matrices.append(Matrix(list(map(int, input(self.prompts[n][i]).split()))))
 assert len(self.matrices[i].dimension) == 2, "Dimension is a list with two values, rows and columns only"
 self.input_matrix(self.matrices[i], n)
 def addition(self):
 """
 uses the matrix class to add two matrices
 Ex:
 1. Add matrices
 2. Multiply matrix by a constant
 3. Multiply matrices
 0. Exit
 Your choice: 1
 Enter size of first matrix: 3 3
 Enter first matrix: 
 1 2 3
 4 5 6
 7 8 9
 Enter size of second matrix: 3 3
 Enter second matrix: 
 1 1 1
 1 1 1
 1 1 1
 The result is: 
 2 3 4
 5 6 7
 8 9 10
 """
 self.input_matrix_n_times(2)
 print('The result is: ')
 print(self.matrices[0] + self.matrices[1], "", sep='\n')
 self.clear_matrices_and_count()
 
 def constant_multiplication(self):
 """
 uses the matrix class to multiply a matrix by a constant
 Ex:
 1. Add matrices
 2. Multiply matrix by a constant
 3. Multiply matrices
 0. Exit
 Your choice: 2
 Enter size of matrix: 3 3
 Enter matrix: 
 1 1 1
 1 1 1
 1 1 1
 Enter constant: 1.5
 The result is: 
 1.5 1.5 1.5
 1.5 1.5 1.5
 1.5 1.5 1.5
 """
 self.input_matrix_n_times(1)
 constant = input('Enter constant: ')
 try:
 constant = int(constant)
 except:
 constant = float(constant)
 print('The result is: ')
 print(constant * self.matrices[0], "", sep='\n')
 self.clear_matrices_and_count()
 def matrix_multiplication(self):
 """
 uses the matrix class to multiply two matrices together
 Ex:
 1. Add matrices
 2. Multiply matrix by a constant
 3. Multiply matrices
 0. Exit
 Your choice: 3
 Enter size of first matrix: 3 3
 Enter first matrix: 
 2 2 2
 2 2 2
 2 2 2
 Enter size of second matrix: 3 3
 Enter second matrix: 
 2 2 2
 2 2 2
 2 2 2
 The result is: 
 12 12 12
 12 12 12
 12 12 12
 """
 self.input_matrix_n_times(2)
 if self.matrices[0].dimension[1] != self.matrices[1].dimension[0]:
 print('The operation cannot be performed.', "", sep='\n')
 return
 
 print('The result is: ')
 print(self.matrices[0] * self.matrices[1], "", sep='\n')
 self.clear_matrices_and_count()
 def transpose_matrix(self):
 """
 Holds the menu for transposing matrices in 4 different ways, 
 the types of transpositions are stored as a switch case in a dictionary, which is called depending on the user's input of 1-4
 Performs transposition and returns the specified transposition requested for.
 """
 print("\n1. Main diagonal\n2. Side diagonal\n3. Vertical line\n4. Horizontal line")
 transposition_type = self.transposition_choice[int(input('Your choice: '))]
 self.input_matrix_n_times(1)
 print('The result is: ')
 print(self.matrices[0].matrix_transposition(transposition_type), '', sep='\n')
 self.clear_matrices_and_count()
 def get_determinant(self):
 """
 acquires and returns the determinant of an nxn matrix
 matrix must have the same number of rows and columns
 """
 self.input_matrix_n_times(1)
 print('The result is: ')
 print(self.matrices[0].determinant(), '', sep='\n')
 self.clear_matrices_and_count()
 def get_inverse(self):
 """
 Acquires the inverse of the matrix, using cofactors and minors
 if determinant is 0, then the matrix doesn't have an inverse form
 """
 self.input_matrix_n_times(1)
 if self.matrices[0].determinant() == 0:
 print("This matrix doesn't have an inverse.", '', sep='\n')
 return
 print('The result is: ')
 print(self.matrices[0].inverse(), '', sep='\n')
 def main(self):
 """
 program runs indefinitely until the exit operation by entering '0' is performed
 """
 while True:
 self.display_choices_and_input()
 
 
MatrixCalculator()
asked Jun 22, 2020 at 1:43
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

The real power of OOP is not that you can use classes as namespaces to bundle functions that belong together. The main power is polymorphism, meaning that you can inherit from classes and use subclasses interchangeably with their parent classes. This means that you want to be able to do something like this:

class UnitaryMatrix(Matrix):
 ...
a = Matrix(...)
u = UnitaryMatrix(...)
a + u

However, this is currently not possible with your class, because you are using a too restrictive check for the type of classes in your operators. Instead of

assert type(other) == Matrix, f"Matrix cannot be added by {type(other)}"

simply use

assert isinstance(other, Matrix), f"Matrix cannot be added by {type(other)}"

isinstance returns true as long as the object is of that type, or a type derived from it. In other words, a UnitaryMatrix is also a Matrix and can be used wherever a Matrix is expected.


I would re-think how you construct your matrix. You want to make the default usecase as simple as possible. I would want to use your class like this:

m = Matrix([[1, 2], [3, 4]])

Without having to specify the dimensions, since they are obvious from the input. Other ways to construct the matrix should be class methods:

m2 = Matrix.from_flattened([1, 2, 3, 4], shape=(2, 2))
m3 = Matrix.zeros(2, 2)

Which you can implement like this:

class Matrix:
 def __init__(self, elements):
 self.matrix = elements
 self.shape = len(elements), len(elements[0])
 ...
 @classmethod
 def from_flattened(self, elements, shape):
 assert len(shape) == 2
 assert len(elements) == shape[0] * shape[1]
 return Matrix([elements[i*shape[0]:(i+1)*shape[0]]
 for i in range(shape[0])])
 @classmethod
 def zeros(self, *shape):
 assert len(shape) == 2
 return Matrix([[0] * shape[1] for _ in range(shape[0])])

Note that I renamed dimension to shape, which is what e.g. numpy uses. For me, dimension should be len(shape), i.e always two in the case of a matrix.

Depending on your usecases, defining a filled and a ones classmethod might also make sense:

 @classmethod
 def filled(self, value, *shape):
 assert len(shape) == 2
 return Matrix([[value] * shape[1] for _ in range(shape[0])])
 @classmethod
 def zeros(self, *shape):
 return Matrix.filled(0, *shape)
 @classmethod
 def ones(self, *shape):
 return Matrix.filled(1, *shape)

Using class methods also allows you to define other special matrices, like the identity matrix:

 @classmethod
 def identity(self, *shape):
 m = Matrix.zeros(*shape)
 for i in range(m.shape[0]):
 m.matrix[i][i] = 1
 return m
>>> Matrix.identity(3, 3)
Matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]])

Using elements == None is not the right way to do it. Use elements is None, as recommended by Python's official style-guide, PEP8.


To ease using your class in an interactive terminal, you should also implement __repr__, which is used as a representation. By convention, the output should be able to construct your class again, i.e. eval(repr(m)) == m. In this case this is rather easy:

class Matrix:
 ...
 def __repr__(self):
 return f"Matrix({self.matrix!r})"
>>> Matrix.zeros(2, 2)
Matrix([[0, 0], [0, 0]])

Instead of using * both for scalar and matrix multiplication, you could implement the (normally unused) operator @, which is called matrix multiplication. In order to do so, simply implement the dunder method __matmul__. Even if you want * to do both things, I would implement __matmul__ anyway and just use self @ other in the definition of __mul__.


Use the built-in sum if you sum up things in a loop:

determinant = sum((-1)**c * matrix[0][c] * self.determinant_helper(self.get_minor(matrix, 0, c))
 for c in range(len(matrix)))

You can also iterate over the entries and indices at the same time using enumerate:

determinant = sum((-1)**i * m_0i * self.determinant_helper(self.get_minor(matrix, 0, i))
 for i, m_0i in enumerate(matrix[0]))

I would consider making the determinant method a property. This allows you to easily make it cached in the future:

class Matrix:
 ...
 @property
 def determinant(self):
 """
 finds the determinant by using the helper function to supply the current matrix of the object
 changes integer floats to integers, and so on
 """
 det = self.determinant_helper(self.matrix)
 return int(det) if det.is_integer() else det
 def inverse(self):
 """
 acquires the inverse form of a matrix by using laplace's expansion
 which is A^-1 (inverse matrix) = 1/determinant * C^T (transposed matrix of all cofactors of all elements in matrix along main diagonal)
 """
 determinant = self.determinant
 ...

In order to cache the result, you just need to use a different decorator (Python 3.8+):

from functools import cached_property
...
class Matrix:
 ...
 @cached_property
 def determinant(self):
 ...

answered Jun 22, 2020 at 7:26
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.