Las matrices son colecciones de números organizados en filas y columnas. Son fundamentales en álgebra lineal, gráficos y resolución de ecuaciones.

En este artículo, aprenderás cómo implementar una matriz en Python, incluyendo sus operaciones y características principales.

¿Qué es una matriz?

Una matriz es una colección de números organizados en filas y columnas que forman una cuadrícula rectangular de tamaño fijo. Estas estructuras son muy útiles en diversas áreas, como el álgebra lineal y los gráficos por computadora.

También puedes usar matrices para representar y resolver sistemas de ecuaciones lineales, entre otras aplicaciones.

Las características fundamentales de una matriz incluyen las siguientes:

  • Tamaño fijo: El número de filas y columnas se determina al crearla y no cambia.
  • Acceso por índices: Los elementos se acceden mediante una pareja de índices enteros representando (fila, columna).
  • Operaciones aritméticas: Las matrices soportan operaciones como suma, resta, multiplicación y escalado.

En Python, las matrices no están integradas como una estructura de datos. Aunque bibliotecas como NumPy proporcionan implementaciones optimizadas, en este artículo construirás tu propia matriz usando listas de listas y reforzarás conceptos de estructuras de datos en Python.

Operaciones comunes en una matriz

A continuación, un resumen de las operaciones que soportará tu matriz:

Operación Descripción
matrix = Matrix(rows, cols) Construye una matriz de tamaño rows × cols con valores en 0.
matrix = Matrix(rows, cols, default) Construye una matriz de tamaño rows × cols con valores en default.
matrix.rows Devuelve el número de filas de la matriz.
matrix.cols Devuelve el número de columnas de la matriz.
matrix.size Devuelve el tamaño de la matriz como una tupla (filas, columnas).
matrix.scale_by(scalar) Escala toda la matriz multiplicando por scalar.
matrix.transpose() Devuelve una nueva matriz transpuesta.
matrix.add(other) Devuelve una nueva matriz con la suma de matrix y other.
matrix + other Devuelve una nueva matriz con la suma de matrix y other.
matrix.subtract(other) Devuelve una nueva matriz con la resta de matrix y other.
matrix - other Devuelve una nueva matriz con la resta de matrix y other.
matrix.multiply(other) Devuelve una nueva matriz con el producto de matrix y other.
matrix * other Devuelve una nueva matriz con el producto de matrix y other.
matrix[i, j] Devuelve el valor en la celda (i, j).
matrix[i, j] = value Asigna value a la celda (i, j).
Matrix.from_list_of_lists(iterable) Construye una nueva matriz a partir de una lista de listas.

Implementación de una matriz en Python

A continuación encontrarás la implementación de la matriz en Python:

Python - matrix.py
from operator import add, sub
from typing import Any, Callable, NamedTuple, Self, Tuple

class Size(NamedTuple):
    """Represents a matrix size as a tuple (rows, cols)."""

    rows: int
    cols: int

class Matrix:
    """Represent a numeric matrix as an m x n rectangular grid."""

    def __init__(self, rows: int, cols: int, default: int = 0) -> None:
        self._rows = rows
        self._cols = cols
        self._data = [[default] * cols for _ in range(rows)]

    @property
    def rows(self) -> int:
        """Return the number of rows."""
        return self._rows

    @rows.setter
    def rows(self, value: int) -> None:
        """Raise AttributeError when trying to assign rows."""
        raise AttributeError("can't set 'rows'")

    @property
    def cols(self) -> int:
        """Return the number of columns."""
        return self._cols

    @cols.setter
    def cols(self, value: int) -> None:
        """Raise AttributeError when trying to assign cols."""
        raise AttributeError("can't set 'cols'")

    @property
    def size(self) -> Size:
        """Return the matrix size as a (rows, cols) tuple."""
        return Size(self.rows, self.cols)

    @size.setter
    def size(self, value: Tuple[int, int]) -> None:
        """Raise AttributeError when trying to assign size."""
        raise AttributeError("can't set 'size'")

    def _validate_index(
        self,
        index: Tuple[int, int]
        ) -> Tuple[int, int]:
        """Validate an index and return it as a (row, col) tuple."""
        if not isinstance(index, tuple) or len(index) != 2:
            raise IndexError("index must be a tuple of two integers")
        row, col = index
        if not (0 <= row < self.rows):
            raise IndexError(
                f"row index out of range: {row}. "
                f"Valid range: 0 to {self.rows - 1}"
            )
        if not (0 <= col < self.cols):
            raise IndexError(
                f"column index out of range: {col}. "
                f"Valid range: 0 to {self.cols - 1}"
            )
        return row, col

    def scale_by(self, scalar: int) -> None:
        """Scale the matrix by a scalar."""
        for i, row in enumerate(self._data):
            for j, _ in enumerate(row):
                self[i, j] *= scalar

    def transpose(self) -> Self:
        """Return the transpose of the current matrix."""
        transposed = type(self)(self.cols, self.rows)
        transposed._data = [list(row) for row in zip(*self._data)]
        return transposed

    def add(self, other: Self) -> Self:
        """Return the sum of this matrix and another matrix."""
        return self.__add__(other)

    def __add__(self, other: Self) -> Self:
        return self._compute(other, operation=add)

    def _compute(self, other: Self, operation: Callable) -> Self:
        if other.__class__ is not self.__class__:
            raise TypeError("expected a Matrix object")
        if other.size != self.size:
            raise ValueError("invalid matrix size")
        matrix = type(self)(self._rows, self._cols)
        for i, row in enumerate(self._data):
            for j, _ in enumerate(row):
                matrix[i, j] = operation(self[i, j], other[i, j])
        return matrix

    def subtract(self, other: Self) -> Self:
        """Return the difference between this matrix and another."""
        return self.__sub__(other)

    def __sub__(self, other: Self) -> Self:
        return self._compute(other, operation=sub)

    def multiply(self, other: Self) -> Self:
        """Return the product of this matrix and another matrix."""
        return self.__mul__(other)

    def __mul__(self, other: Self) -> Self:
        if other.__class__ is not self.__class__:
            raise TypeError("expected a Matrix object")
        if self.cols != other.rows:
            raise ValueError("invalid matrix size")
        matrix = type(self)(self.rows, other.cols)
        for i in range(self.rows):
            for j in range(other.cols):
                for k in range(other.rows):
                    matrix[i, j] += self[i, k] * other[k, j]
        return matrix

    @classmethod
    def from_list_of_lists(cls, iterable: list[list[Any]], /) -> Self:
        """Return a new matrix built from a list of lists."""
        if len(set(len(row) for row in iterable)) > 1:
            raise ValueError("invalid matrix size")
        matrix = cls(rows=len(iterable), cols=len(iterable[0]))
        for i, rows in enumerate(iterable):
            for j, value in enumerate(rows):
                matrix[i, j] = value
        return matrix

    def __getitem__(self, index: Tuple[int, int]) -> Any:
        row, col = self._validate_index(index)
        return self._data[row][col]

    def __setitem__(self, index: Tuple[int, int], value: Any) -> None:
        row, col = self._validate_index(index)
        self._data[row][col] = value

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}"
            f"(rows={self._rows}, cols={self._cols})"
        )

    def __str__(self) -> str:
        return (
            f"{self.__class__.__name__}"
            f"({' '.join(str(row) for row in self._data)})"
        )

El atributo ._data es una lista de listas que almacena los valores de la matriz. Cada sublista representa una fila de la matriz. Los atributos ._rows y ._cols almacenan las dimensiones, y se exponen como propiedades de solo lectura.

El método auxiliar privado ._validate_index() verifica que el índice sea una tupla de dos enteros dentro del rango válido de la matriz. Esto reemplaza la dependencia externa pyadt.utils.validate_index del borrador original.

En las anotaciones de tipo, la clase usa Self para indicar que los métodos que combinan, transforman o construyen matrices devuelven instancias del mismo tipo de la clase actual. Esto mejora la compatibilidad con subclases y hace el tipado más preciso.

Los métodos principales te permiten ofrecer las siguientes funcionalidades:

  • .scale_by(): multiplica todos los elementos de la matriz por un escalar.
  • .transpose(): devuelve una nueva matriz transpuesta intercambiando filas y columnas.
  • .add(): devuelve una nueva matriz con la suma de dos matrices del mismo tamaño.
  • .subtract(): devuelve una nueva matriz con la resta de dos matrices del mismo tamaño.
  • .multiply(): devuelve una nueva matriz con el producto de dos matrices compatibles.
  • .from_list_of_lists(): método de clase que construye una matriz a partir de una lista de listas y retorna una instancia del tipo actual.

Los métodos especiales te permiten ofrecer las siguientes funcionalidades:

  • .__getitem__() y .__setitem__(): permiten acceder y asignar valores con la sintaxis matrix[i, j].
  • .__add__(), .__sub__() y .__mul__(): permiten usar los operadores +, - y *.
  • .__repr__() y .__str__(): devuelven representaciones de cadena legibles.

Ejemplo de uso de la matriz

En esta sección verás ejemplos prácticos de cómo crear y manipular instancias de tu clase Matrix.

Inicialización

Para crear una Matrix, necesitas indicar el número de filas y columnas. Opcionalmente, puedes especificar un valor por defecto:

Python REPL
>>> matrix = Matrix(3, 3)
>>> print(matrix)
Matrix([0, 0, 0] [0, 0, 0] [0, 0, 0])

>>> matrix = Matrix(2, 3, 1)
>>> print(matrix)
Matrix([1, 1, 1] [1, 1, 1])

También puedes construir una matriz a partir de una lista de listas:

Python REPL
>>> matrix = Matrix.from_list_of_lists([[1, 2], [3, 4], [5, 6]])
>>> print(matrix)
Matrix([1, 2] [3, 4] [5, 6])

Acceso y modificación de elementos

Puedes leer y escribir elementos con la sintaxis matrix[fila, columna]:

Python REPL
>>> matrix = Matrix(2, 2)
>>> matrix[0, 0] = 10
>>> matrix[0, 1] = 20
>>> matrix[1, 0] = 30
>>> matrix[1, 1] = 40
>>> print(matrix)
Matrix([10, 20] [30, 40])

>>> matrix[0, 0]
10
>>> matrix[1, 1]
40

Propiedades de la matriz

Las propiedades .rows, .cols y .size devuelven las dimensiones de la matriz y son de solo lectura:

Python REPL
>>> matrix = Matrix(4, 3)

>>> matrix.rows
4
>>> matrix.cols
3
>>> matrix.size
Size(rows=4, cols=3)

Escalado

El método .scale_by() multiplica todos los elementos por un escalar:

Python REPL
>>> matrix = Matrix(2, 2, 1)
>>> print(matrix)
Matrix([1, 1] [1, 1])

>>> matrix.scale_by(5)
>>> print(matrix)
Matrix([5, 5] [5, 5])

Transposición

El método .transpose() devuelve una nueva matriz donde las filas se convierten en columnas:

Python REPL
>>> matrix = Matrix.from_list_of_lists([[1, 2, 3], [4, 5, 6]])
>>> print(matrix)
Matrix([1, 2, 3] [4, 5, 6])

>>> transposed = matrix.transpose()
>>> print(transposed)
Matrix([1, 4] [2, 5] [3, 6])

Operaciones aritméticas

Puedes sumar, restar y multiplicar matrices usando métodos o los operadores en Python +, - y *:

Python REPL
>>> matrix_a = Matrix(2, 2, 10)
>>> matrix_b = Matrix(2, 2, 3)

>>> result = matrix_a + matrix_b
>>> print(result)
Matrix([13, 13] [13, 13])

>>> result = matrix_a - matrix_b
>>> print(result)
Matrix([7, 7] [7, 7])

Para la multiplicación, las dimensiones deben ser compatibles:

Python REPL
>>> matrix_a = Matrix.from_list_of_lists([[1, 2], [3, 4]])
>>> matrix_b = Matrix.from_list_of_lists([[5, 6], [7, 8]])

>>> result = matrix_a * matrix_b
>>> print(result)
Matrix([19, 22] [43, 50])

Representación en cadena

Los métodos .__str__() y .__repr__() proporcionan representaciones legibles:

Python REPL
>>> matrix = Matrix(2, 3, 1)

>>> print(matrix)
Matrix([1, 1, 1] [1, 1, 1])

>>> print(repr(matrix))
Matrix(rows=2, cols=3)

Conclusión

Has aprendido cómo implementar una matriz que proporciona operaciones esenciales como acceso por índices, escalado, transposición, suma, resta y multiplicación.

Este conocimiento te sirve de base para comprender cómo se representan datos bidimensionales y para abordar problemas de álgebra lineal y gráficos por computadora.