В презентации будет рассмотрен в основном 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 – номером столбца (в этом списке выбираем элемент).
(Фраза Линуса Торвальдса;
лит. пер.: «Хватит дешёвой болтовни. Покажите мне код.»)
это "list comprehensions"
Напоминаю синтаксис данного синтаксического сахара.
Для двумерного списка: l1 = [[l for l in r] for r in rs]
.
Здесь мы проходимся по всем элементам r
объекта rs
, а затем по всем элементам каждого из этих элементов r
, которые становятся элементами len(rs)
вложенных списков. Тут нужно заметить, что это был вообще неполезный пример почти в любой ситуации (да, мы получили объект с теми же элементами), однако к каждому добавляемому элементу можно применять функции и проч.
Поэтому для каждого отдельного выражения вложенной абстракции имеем следующий общий синтаксис.
В целом есть две ситуации:
l1 = [f1(e) if cond(e) else f2(e) for e in iterable]
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="")
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="")
### # # ### # # # # # # # #
# Вот так можно ввести матрицу
# - используем *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
# Неформатированный вывод матриц - дело несложное
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]
работать не будет, даже если Вам очень хочется так последовательно распаковывать матрицы.
# Можно реализовать вывод и таким способом:
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]
)
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)])
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
# Протестируем
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-е есть встроенный оператор, отведённый под перемножение двух матриц. Оператор @
.
Появился он с PEP 465. Замечу: это Python 3.5+.
Оператор есть; имплементация отсутствует (опять-таки - обычная философия Python «batteries included» здесь работает, правда, не на 100%))
Тем не менее, с этим оператором мы можем очень легко расширить наш класс Matrix
.
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]
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)
# Протестируем умножение:
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 часто для матриц удобнее использовать именно вложенные массивы, а не встроенный класс матриц, т.к. они поддерживают больше операций.
string
- Format Specification Mini-Language# >>> ...