Exercism: Class representation of a matrix in Python
I would make a few suggestions, rather than just saving the input string it would be better, as you suggest, to save the output of the get_rows function in memory, so you don't have to recreate it every time.
Similarly when you want an individual row or column, it seems inefficient to gather all the rows, (and then all the columns) just to reject all but one of them.
Your comments say produces i-th (column/row)... but the variable is referred to as index
Minor points:
You don't need to save rows as a variable in _get_columns, you can just use [list(row) for row in zip(*self._get_rows())]
you could reduce _get_rows to one line : [[int(char) for char in row_string.split()] for row_string in self.matrix_string.split('\n')]
I'm assuming that you don't need to check if the input is invalid, because that isn't part of the testing suite. However you may want to do it anyway if you plan on using this class again.
In terms of style the code is good. The first comment under class Matrix doesn't really say anything and there are more newlines than are necessary, but other than that everything looks good.
You could try something like:
class Matrix:
    &quot;&quot;&quot;Class representation of an integer matrix indexed from 1
    ...

    Attributes
    ----------
    rows: List[List(int)]
        A list of the rows of values in the matrix.

    Methods
    -------
    row(self, i)
        returns the i-th row.

    column(self, i)
        returns the i-th column.
    &quot;&quot;&quot;

    def __init__(self, matrix_string):
        &quot;&quot;&quot;Save a list of rows from the input string.
        - matrix_string: A space and new line seperated string of values
        in the matrix.
        &quot;&quot;&quot;
        self.rows = self._get_rows(matrix_string)

    def _get_rows(self, matrix_string):
        &quot;&quot;&quot;Produce a list of rows of the matrix.
        - matrix_string: A space and new line seperated string of values
        in the matrix.
        &quot;&quot;&quot;
        rows = []
        for row_string in matrix_string.split('\n'):
            rows.append([int(char) for char in row_string.split()])
        return rows

    def _get_columns(self):
        &quot;&quot;&quot;Produce a list of columns of the matrix.&quot;&quot;&quot;
        return [list(row) for row in zip(*self.rows)]

    def row(self, i):
        &quot;&quot;&quot;Produce i-th row of the matrix with first index being 1.&quot;&quot;&quot;
        return self.rows[i - 1]

    def column(self, i):
        &quot;&quot;&quot;Produce i-th column of the matrix with first index being 1.&quot;&quot;&quot;
        return [row[i - 1] for row in self.rows]


or you could drop the _get_rows and change the __init__ to:
    def __init__(self, matrix_string):
        &quot;&quot;&quot;Save a list of rows from the input string.
        - matrix_string: A space and new line seperated string of values
        in the matrix.
        &quot;&quot;&quot;
        self.rows = [[int(x) for x in row.split()] for row in matrix_string.split(&quot;\n&quot;)]


In terms of performance:
using cProfile and the following script:
if __name__ == &quot;__main__&quot;:
    for _ in range(100000):
        matrix = Matrix(&quot;1 2 3 4\n5 6 7 8\n9 8 7 6\n1000 2000 3000 4000&quot;)
        for i in range(1, 5):
            matrix.column(i)
            matrix.row(i)


Produces for the old matrix definition:
12900228 function calls (12900222 primitive calls) in 10.239 seconds
For the my definition:
1900228 function calls (1900222 primitive calls) in 1.359 seconds