Част 4 от моята поредица за програмиране за пакета Python swiftex
за писане на математика в Jupyter. Това е толкова бързо, колкото с молив и хартия.
За предишната част следвайте тази връзка.
В моята скорошна статия „Числени производни направени правилно“ използвах следната формула, като първоначално я написах в LaTeX. Ако сте използвали LaTeX преди, тогава знаете колко е досадно да пишете такива изрази. И ако искате да го редактирате, трябва да започнете отначало, защото е толкова сложно. В днешната статия ще подобрим пакета swiftex
така, че писането на матрици в Jupyter и експортирането им в LaTeX е просто парче торта.
Константни матрици
Започваме с възможно най-лесните матрици: постоянните. Например, напишете тест за матрица 2x2 с всички записи, зададени на 1:
class TestMatrix(unittest.TestCase): def test_constant_matrix(self):
A = Matrix(2, 2, default=1)
expected = r'\left(' \ r'\begin{matrix}' \ r'1 & 1 \\' \ r'1 & 1 ' \ r'\end{matrix}' \ r'\right)'
self.assertEqual(expected, A)
Входът трябва да бъде само един ред, съответстващ на 6 реда от LaTeX изход. Като реализация въвеждаме друг клас Matrix
, наследяващ от Symbol
:
class Matrix(Symbol): def __init__(self, nrows, ncols, default='', **kwargs):
self.values = {} self.nrows = nrows self.ncols = ncols for i in range(nrows): for j in range(ncols): self.values[(i, j)] = Symbol(str(default)) super().__init__(str(self), **kwargs) def __str__(self): s = r'\left(\begin{matrix}' for i in range(self.nrows): for j in range(self.ncols): s += str(self.values[i, j]) if j < self.ncols - 1: s += ' & ' elif i < self.nrows - 1: s += r'\\' s += r'\end{matrix}\right)' return s def __repr__(self): return self.__str__()
Реших да заменя функциите за преобразуване и представяне на низове от родителския клас, защото, добре, матриците са малко по-сложни. :-) Записите на матрицата са всички Symbol
обекти, събрани в речник и могат да бъдат достъпни чрез индекс 2-кортеж. Това е всичко за постоянните матрици, но това все още не е много полезно.
Задаване на стойности на матрицата
Имаме нужда от начин да зададем единични стойности на матрица. Това трябва да изглежда така в следния тест:
def test_set_matrix_by_index(self): A = Matrix(2, 2, default=0) A[0, 1] = 1 A[1, 0] = 1 expected = r'\left(' \ r'\begin{matrix}' \ r'0 & 1 \\' \ r'1 & 0 ' \ r'\end{matrix}' \ r'\right)'
self.assertEqual(expected, A)
Тук инициализираме постоянна нулева матрица и заменяме 2 записа. Изпълнението на теста сега дава следната грешка:
A[0, 1] = 1
TypeError: 'Matrix' object does not support item assignment
Трябва да внедрим метода __setitem__
, за да работи това:
def __setitem__(self, idx, value):
self.values[idx] = Symbol(str(value))
С това на място нашата 2x2 матрица A изглежда така:
Matrix Slices
Добре, засега има само незначително предимство пред писането на чист LaTeX. Но това се променя веднага щом позволим срезове като аргументи. Помислете за следния тест:
def test_matrix_slice_by_index(self):
A = Matrix(3, 3, default=0) A[1, :] = 1
expected = B = Matrix(3, 3, default=0) B[1, 0] = 1 B[1, 1] = 1 B[1, 2] = 1
self.assertEqual(expected, A)
Тук дефинираме матрица 3x3 A
с всички стойности 0 и след това задаваме всички стойности на втория ред на 1. Това означава операторът за нарязване :
. Така че нашата реализация трябва да вземе предвид, че аргументите на оператора []
могат да бъдат срезове, които са вградени обекти в Python. В момента изпълнението на нашия тест дава грешката:
self.values[idx] = Symbol(str(value))
TypeError: unhashable type: 'slice'
Това е така, защото все още не сме подготвени за резени. Можем да се адаптираме, като променим изпълнението на метода __setitem__
:
def __setitem__(self, idx, value):
assert len(idx) == 2
if isinstance(idx[0], slice):
if idx[0].start is None:
start = 0
else:
start = idx[0].start
if idx[0].stop is None:
stop = self.nrows
else:
stop = idx[0].stop
row_inds = list(range(start, stop))
else:
row_inds = [idx[0]]
if isinstance(idx[1], slice):
if idx[1].start is None:
start = 0
else:
start = idx[1].start
if idx[1].stop is None:
stop = self.ncols
else:
stop = idx[1].stop
col_inds = list(range(start, stop))
else:
col_inds = [idx[1]]
for i in row_inds:
for j in col_inds:
self.values[i, j] = Symbol(str(value))
Срезовите обекти имат start
и stop
атрибут, което означава индексите, откъдето да започне и спре среза. Задаването на един от тях на None
съответства на пропускане на стойността в израза на среза. Например имаме
3:7 <==> start == 3, stop == 7 :5 <==> start == None, stop == 5 : <==> start == None, stop == None
Всички тестове преминават успешно, но това е много грозен код и голяма част от него е излишен. Преди да обясня какво прави кодът, забележете, че двата if-блока са почти еднакви. Перфектен за рефакторинг. Така че нека напишем частен метод _expand_indices
:
def _expand_slice(self, slice_or_ind, max_vals):
if isinstance(slice_or_ind, slice):
if slice_or_ind.start is None:
start = 1
else:
start = slice_or_ind.start
if slice_or_ind.stop is None:
stop = max_vals
else:
stop = slice_or_ind.stop
inds = list(range(start, stop))
else:
inds = [slice_or_ind]
return inds
и заменете if-блоковете с извиквания на този метод.
def __setitem__(self, idx, value):
assert len(idx) == 2
row_inds = self._expand_slice(idx[0], self.nrows)
col_inds = self._expand_slice(idx[1], self.ncols)
for i in row_inds:
for j in col_inds:
self.values[i, j] = Symbol(str(value))
Сега е малко по-ясно какво се случва. В случай, че единият или и двата аргумента (idx
е кортеж!) е срез (като :
или 2:
или :-1
), _expand_slice
преобразува среза в списък от индекси, който съответства на този срез. След това преминаваме през всички индекси на среза (или може би само един като за единичен запис) и присвояваме value
на всички тези матрични записи.
Избирането на срезове и присвояването на постоянна стойност е хубаво, но би било още по-хубаво, ако можем да присвоим и последователност, както в следващия тест.
def test_matrix_slice_with_sequence(self):
A = Matrix(3, 3, default=0) A[1, :] = 1, 2, 3
expected = B = Matrix(3, 3, default=0) B[1, 0] = 1 B[1, 1] = 2 B[1, 2] = 3
self.assertEqual(str(expected), str(A))
Това може лесно да се вземе предвид чрез модифициране на изпълнението на __setitem__
:
def __setitem__(self, idx, value):
assert len(idx) == 2
row_inds = self._expand_slice(idx[0], self.nrows)
col_inds = self._expand_slice(idx[1], self.ncols)
k = 0
for i in row_inds:
for j in col_inds:
if hasattr(value, '__len__'):
self.values[i, j] = Symbol(str(value[k]))
else:
self.values[i, j] = Symbol(str(value))
k += 1
Ако стойността, която се опитваме да присвоим, е итерируема (т.е. може да се използва като аргумент на вградената функция len
), тогава присвояваме всяка стойност една след друга, в противен случай правим същото както преди.
брекети
Преди да завърша тази статия, има още едно нещо, на което искам да се спра, което може да се появи неочаквано, но ще ми трябва за последния пример в този текст. Досега, когато казвам
(a + b)**2
Резултатът е
Това не е това, което възнамеряваме. Това е така, защото скобите на Python не могат да бъдат разрешени и се използват само от интерпретатора за определяне на приоритета на оператора. Като бързо решение, нека дефинираме изрична функция за писане на скоби, както в следния тест:
def test_brace(self): actual = brace(a+b)**2 self.assertEqual('(a + b)^2', actual)
Изпълнението е малка функция:
def brace(expr): return r'\left(' * expr * r'\right)'
Нещо по-сложно
И накрая, нека приложим нашия нов клас към по-сложния случай на употреба, споменат в началото:
добиви
И ако някога имате нужда от същото нещо като чист LaTeX, просто приложете функцията print
:
print(A) Out: \left(\begin{matrix}1 & 1 & 1 & 1 & 1 & 1\\- p & - p + 1 & - p + 2 & \dots & q - 1 & q\\\left( - p \right)^{2} & \left( - p + 1 \right)^{2} & \left( - p + 2 \right)^{2} & \dots & \left( q - 1 \right)^{2} & q^{2}\\\dots & \dots & \dots & \dots & \dots & \dots\\\left( - p \right)^{p + q - 1} & \left( - p + 1 \right)^{p + q - 1} & \left( - p + 2 \right)^{p + q - 1} & \dots & \left( q - 1 \right)^{p + q - 1} & q^{p + q - 1}\\\left( - p \right)^{p + q} & \left( - p + 1 \right)^{p + q} & \left( - p + 2 \right)^{p + q} & \dots & \left( q - 1 \right)^{p + q} & q^{p + q}\end{matrix}\right)
Пълен изходен код
Както винаги, можете да получите пълния изходен код от моето хранилище на GitHub. Просто проверете
git checkout 384d586d9a2f
за да видите състоянието в края на тази статия.
Благодаря за четенето и очаквайте следващата сесия!
Повече съдържание в plainenglish.io. Регистрирайте се за нашия безплатен седмичен бюлетин тук.