Матрицы и Python


fedor1113

Структуры данных

«В рубежах поденной структуры
Переполох -
Сбой фазы огорошил цепь.»
- группа «Алиса», «Пересмотри»

В презентации будет рассмотрен в основном Python 3.7 (конечно, ничего крайне специфичного не будет, поэтому экстраполировать для других версий (в том числе - Python 2.x) не составит труда).

В математических расчётах мы используем структуры типа $ \begin{bmatrix} x_{11} & x_{12} \\ x_{21} & x_{22} \end{bmatrix} $. Резонно надеяться найти наиболее похожий на эту структуру объект для её хранения.

Обычно в разных языках программирования используется АТД двумерного массива.

В Питоне, как известно, вообще вместо классического массива используется структура списка. Поэтому для хранения матриц мы используем… правильно, двумерный список.

Лирическое отступление: конечно, в виде двумерных списков можно представить любую таблицу данных. Также можно создавать и вложенные, многомерные списки.

Матрицы <=> Списки

По сути – тут ничего сложного. Это просто список списков.

Каждый внутренний список – строка матрицы. Тогда при вызове элемента дважды используем индекс (просто последовательно применяем; тут это является примером method chaining, т.к. всё в Питоне – объект, просто используется особый, зарезервированный спец. синтаксис квадратных скобок): m[j][k] – вернёт элемент матрицы m*j,k*. Здесь j является номером строки (выбираем один из внутр. списков), k – номером столбца (в этом списке выбираем элемент).

«Talk is cheap. Show me the code.»

(Фраза Линуса Торвальдса;

лит. пер.: «Хватит дешёвой болтовни. Покажите мне код.»)

Многомерные списки

Абстракции вложенных списков

это "list comprehensions"

Напоминаю синтаксис данного синтаксического сахара. Для двумерного списка: l1 = [[l for l in r] for r in rs]. Здесь мы проходимся по всем элементам r объекта rs, а затем по всем элементам каждого из этих элементов r, которые становятся элементами len(rs) вложенных списков. Тут нужно заметить, что это был вообще неполезный пример почти в любой ситуации (да, мы получили объект с теми же элементами), однако к каждому добавляемому элементу можно применять функции и проч.

Поэтому для каждого отдельного выражения вложенной абстракции имеем следующий общий синтаксис.

В целом есть две ситуации:

  • Для каждого элемента итерируемого объекта, если выполняется условие, добавляем в список f1() от этого элемента, иначе - f2() от этого элемента:
    l1 = [f1(e) if cond(e) else f2(e) for e in iterable]
    
  • Для каждого элемента итерируемого объекта, если выполняется условие, добавляем в список f1() от этого элемента, иначе - пропускаем его:
    l1 = [f1(e) for e in iterable if cond(e)]
    
Наглядное «разворачивание» абстракции списков

Разобранная выше сокращённая запись l1 = [f1(e) if cond(e) else f2(e) for e in iterable] будет эквивалентна следующему коду.

l1 = []
for e in iterable:
    if cond(e):
        l1.append(f1(e))
    else:
        l1.append(f2(e))
Более сложный пример

Списковое включение:

dimensions = (7, 6); d = dimensions
s = "111000101000111000100101100010100010000100"
l1 = [[s[r * d[1] + c] for c in range(d[1])] for r in range(d[0])]
for r in l1:
    print(*r, sep="")

Развёрнутый вариант:

dimensions = (7, 6); d = dimensions
s = "111000101000111000100101100010100010000100"
l1 = []
[s[r * d[1] + c] for c in range(d[1])] for r in range(d[0])
for r in range(d[0]):
    l1.append([])
    for c in range(d[1]):
        l1[r].append(s[r * d[1] + c])
for r in l1:
    print(*r, sep="")
In [2]:
dimensions = (7, 6); d = dimensions
s = "111000101000111000100101100010100010000100"
l1 = [["#" if s[r * d[1] + c] == "1"
       else " "
       for c in range(d[1])] 
      for r in range(d[0])]
for r in l1:
    print(*r, sep="")
###   
# #   
###   
#  # #
#   # 
#   # 
   #  

Ввод

In [1]:
# Вот так можно ввести матрицу
# - используем *list comprehension*
# (то, что часто по-русски крайне неудачно
# называют "генераторы списков")
# Лучшим переводом 
# (хотя подходящим лишь по историческим причинам)
# будет "абстракция списков"
# или   "списковое включение"
rows, cols = map(int, input().split())
matrix = [list(map(int, input().split())) for r in range(rows)]
4 5
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20

Вывод

In [2]:
# Неформатированный вывод матриц - дело несложное
srepr = "".join((" ".join(map(str, r)) + "\n"
                         for r in matrix))
print(srepr)
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20

Кроме строкового метода join() полезна в работе с матрицами, конечно, и незаменимая вообще в работе со списками и кортежами их распаковка - через унарный *-оператор.

Однако нужно помнить: к сожалению (или к счастью - т.к. при добавлении подобной опции появляются проблемы с двусмысленностью/неочевидностью синтаксиса), пока Python не поддерживает распаковку итерируемого объекта внутри выражений list comprehension. Т.е. [*l for l in lst] работать не будет, даже если Вам очень хочется так последовательно распаковывать матрицы.

In [3]:
# Можно реализовать вывод и таким способом:
for r in matrix:
    print(*r)
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20

Форматирование

Для форматированного вывода лучше всего, конечно, использовать одну из опций языка Python, специально отведённых для форматирования строк. Метод строк format() или форматированную строку f"".

Второй метод быстрее и удобнее, но появился только в версии 3.6.

Форматирование строк имеет свой особый синтаксис, который имеет смысл изучить. Для работы с числовыми матрицами полезны синтаксические конструкции, позволяющие выводить тип float с заданной точностью, выводить строку заданной длины (заполняющие её заданным символом).

Последнее очень важно для форматированного вывода таблиц. Для форматируемой переменной нужно в строке задать опцию >длина, чтобы строка заполнилась пробелами слева. Т.е. f"{variable:>lngth}". Пробел - заполнитель по умолчанию. Вообще можно указать явно: f"{variable: >lngth}".

На всякий случай: для вывода чисел с плавающей точкой с заданной точностью используется флаг f. Пример: f"{variable:.4f}" - выводит с точностью 4 знака после запятой.

Также полезной функцией форматирования является вывод в разных системах счисления (поддерживаются двоичная, восьмиричная, десятиричная и шестнадцатиричная). Всё это хорошо описано в документации Python.

Введение специального класса для матриц

Развивая тему method chaining, хотелось бы отметить, что Python позволяет нам использовать (вводить) синтаксис с квадратными скобками (англ. brackets) в наших собственных классах через специальные методы (например, метод __getitem__(self, key) -> obj[key])

In [4]:
class Matrix:
    """A simple wrapper around a two-dimensional list"""
    """Простая обёртка вокруг 2-мерного списка - пример"""
    def __init__(self, lst=[[]]):
        self._lst = lst
    # ...
    def __getitem__(self, index):
        return self._lst[index]

    def __setitem__(self, index, val):
        self._lst[index] = val

    def __delitem__(self, index):
        del self._lst[index]

    def __iter__(self):
        return self._lst.__iter__()

    def __len__(self):
        return len(self._lst)

    def __contains__(self, item):
        return item in self._lst

    def __add__(self, other):
        assert type(other) is Matrix
        return Matrix([[e1 + e2 for e1, e2 in zip(r1, r2)]
                       for r1, r2 in zip(self._lst, other)])
In [5]:
class Matrix(Matrix):
    def __str__(self):
        from functools import partial
        def _frmt(el, max_len=0):
            # r = f"{el:>{max_len}}"  # PEP 498 -- Literal String Interpolation
            # с Python 3.6
            r = "{:>{}}".format(el, max_len)  # Медленнее, сложнее
            
            return r
        
        srepr = ""
        max_len = 0
        max_len = max((len(str(elem))
                       for row in self._lst
                       for elem in row),
                      default=0)
        _frmt = partial(_frmt, max_len=max_len)
            
        for row in self._lst:
            srepr += " ".join(map(_frmt, map(str, row))) + "\n"
        
        return srepr
In [6]:
# Протестируем
m1 = Matrix([[11, 12, 13, 14], [21, 22, 23, 24],
                [31, 32, 33, 34], [41, 42, 43, 44]])

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

print(m1)
print(m2)
print(m1[0][0])
print(m1 + m2)
11 12 13 14
21 22 23 24
31 32 33 34
41 42 43 44

1 1 1 1
2 2 2 2
3 3 3 3
4 4 4 4

11
12 13 14 15
23 24 25 26
34 35 36 37
45 46 47 48

Комментарии

Мы можем таким образом легко расширять встроенный функционал списков. Как видно из примера выше. Можно добавить любые необходимые операции с матрицами.

Мы, конечно, могли бы сделать матрицу подклассом списка, но для наглядности переписали всё сами.

Умножение матриц в Python

У работы с матрицами в Python-е есть одна очень интересная и малоизвестная особенность. В Python-е есть встроенный оператор, отведённый под перемножение двух матриц. Оператор @.

Появился он с PEP 465. Замечу: это Python 3.5+.

Оператор есть; имплементация отсутствует (опять-таки - обычная философия Python «batteries included» здесь работает, правда, не на 100%))

Тем не менее, с этим оператором мы можем очень легко расширить наш класс Matrix.

In [7]:
class Matrix_extended(Matrix):
    def __init__(self, lst=[[]]):
        super().__init__(lst)
        self.update_order()
        
    def update_order(self):
        self.order = (len(self._lst), len(self._lst[0]))
        # количество строк, количество столбцов
    
    def __delitem__(self, index):
        super().__delitem__(index)
        self.update_order()
    
    def __len__(self):
        # return len([r for r in self._lst])
        # Пожалуй, больше подходит для матриц
        return self.order[0] * self.order[1]
In [8]:
class Matrix_extended(Matrix_extended):
    def __matmul__(self, multiplier):
        if self.order[1] != multiplier.order[0]:
            raise ArithmeticError("Матрицы не согласованы.")
        
        return Matrix([[sum(a*b for a, b in zip(srow, mcol))
                 for mcol in zip(*multiplier._lst)]
                for srow in self._lst])

    def __rmatmul__(self, multiplicand):
        if multiplicand.order[1] != self.order[0]:
            raise ArithmeticError("Матрицы не согласованы.")
        
        return Matrix([[sum(a*b for a, b in zip(mrow, scol))
                 for scol in zip(*self._lst)]
                for mrow in multiplicand._lst])
    
    def __imatmul__(self, multiplier):
        """Для a @= b"""
        self._lst = self @ multiplier
        return Matrix(self._lst)
In [9]:
# Протестируем умножение:

m1 = Matrix_extended([[11, 12, 13, 14], [21, 22, 23, 24],
                [31, 32, 33, 34], [41, 42, 43, 44]])
    
m2 = Matrix_extended([[1, 1, 1, 1], [2, 2, 2, 2],
             [3, 3, 3, 3], [4, 4, 4, 4]])

print(m1)
print(m2)

print(m1 @ m2)
11 12 13 14
21 22 23 24
31 32 33 34
41 42 43 44

1 1 1 1
2 2 2 2
3 3 3 3
4 4 4 4

130 130 130 130
230 230 230 230
330 330 330 330
430 430 430 430

Заключение


С этой информацией, думаю, программист уже имеет представление о многих элементарных функциях Питона, полезных в работе с матрицами; можно закончить этот небольшой tutorial.

В заключение, для быстрой, профессиональной работы с матрицами лучше использовать специальные модули (а не встроенные средства языка) - numpy со встроенными массивами, которые поддерживают умножение на скаляр и тому подобные операции, - одно из самых популярных решений. Кстати, как ни странно, в numpy часто для матриц удобнее использовать именно вложенные массивы, а не встроенный класс матриц, т.к. они поддерживают больше операций.

Полезные ресурсы

In [11]:
# >>> ...