Macierze: operacje elementarne

Tablice i pętle

Tablice w Pythonie noszą nazwę list. Używamy ich następująco:

  • Stworzenie lity (tablicy):

    • z określonymi elementami: tab = [1, 2, 3, 4, 5, 6, 10]

    • o określonej liczbie elementów: tab = [0] * 100

  • Przypisanie albo odczytanie \(k\)-tego elementu listy (indeksy od 0): tab[k] = 42 lub tab[k]

  • Sprawdzenie długości lity: len(tab)

Python ma pętlę while, która powtarza podany zestaw instrukcji tak długo, jak długo podany warunek jest spełniony. Do warunków możemy równości (\(a = b\) to a == b, \(a \ne b\) to a != b), nierówności (\(a < b\) to a < b, \(a \le b\) to a <= b, podobnie nierówności w drugą stronę) i spójników logicznych and oraz or.

Ilustracja składni instrukcji while wygenerowana przez Gemini

Ważne

Napisanie:

while warunek:
    instrukcja1
    instrukcja2
    ...

Powoduje powtarzanie podanych instrukcji (1, 2, …) tak długo, jak długo warunek jest prawdziwy. Warunek musi być wyrażeniem logicznym — takim o wartości True albo False.

Na przykład:

pierwiastek = 0
while pierwiastek * pierwiastek < 1997:
    print(f"{pierwiastek} to nadal za mało")
    pierwiastek += 1
print("{pierwiastek} jest pierwszą liczbą, której kwadrat przekracza 1997")

Podstawowa tabelka liczb

Zaczniemy od napisania klasy, która pozwoli nam przechowywać dwuwymiarową tabelkę liczb:

  • m = Matrix(nr, nc) utworzy wypełnioną zerami macierz o nr wierszach i nc kolumnach.

  • m.nrows() zwróci liczbę wierszy.

  • m.ncols() zwróci liczbę kolumn.

  • set(r, c, v) ustawi w r-tym wierszu k-tej kolumny wartość v.

  • get(r, c) zwróci wartość w macierzy w r-tym wierszu k-tej kolumny.

Kod startowy, który możesz wkleić do nowego pliku (nazwij go matrix.py):

class Matrix:
    def __init__(self, rows, cols):
        ...  # TODO: zadanie 1

    def nrows(self):
        ...  # TODO: zadanie 3

    def ncols(self):
        ...  # TODO: zadanie 3

    def get(self, row, col):
        ...  # TODO: zadanie 3

    def set(self, row, col, val):
        ...  # TODO: zadanie 3

Zadanie 1: tworzenie pustej macierzy

Do pamiętania macierzy w naszym programie użyjemy listy list. Macierz

\[\begin{split}M = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}\end{split}\]

Będziemy pamiętać jako listę list:

lista = [[1, 2, 3], [4, 5, 6]]

Czyli lista zawiera kolejne wiersze macierzy:

  • lista[0] to pierwszy wiersz [1,2,3], jego pierwszy element (czyli \(m_{1\,1}\)) to lista[0][0].

  • lista[1] to drugi wiersz [1, 2, 3], jego trzeci element (czyli \(m_{2\,3}\)) to lista[1][2].

W kodzie startowym uzupełnij funkcję Matrix.__init__, w której wewnątrz klasy w self._data zapamiętasz listę list samych zer o odpowiednich rozmiarach.

Uwaga

Funkcje wewnątrz klasy wołamy (prawie) zawsze pisząc zmienna.funkcja(). Dlatego funkcje wewnątrz klasy MojaKlasa będziemy pisać jako MojaKlasa.moja_funkcja(). Funkcje spoza klasy będziemy pisać bez kropki - moja_funkcja().

Możesz potem przetestować swoje rozwiązanie, dopisując na końcu pliku stworzenie zmiennej zawierającej macierz \(\mathbb{R}^{4 \times 6}\):

m = Matrix(4, 6)

Zadanie 2: rozmiar macierzy

Przypomnienie: len(l) to liczba elementów bezpośrednio w liście l. Zagadka: ile wynosi len([1,2,[3,4,5]]), dlaczego?

Teraz: jak możemy powiedzieć tylko na podstawia listy wierszy w self._data, jakie są wymiary macierzy?

Uzupełnij kod funkcji Matrix.nrows() i Matrix.ncols().

Zadanie 3: dostęp do elementów macierzy

Uzupełnij teraz kod funkcji Matrix.get i Matrix.set.

Opcjonalnie: dodaj do każdej z nich sprawdzenie, czy r i c nie są za duże, lub za małe:

if not (0 <= row < self.nrows() and 0 <= col < self.ncols()):
    raise IndexError(f"Niepoprawny index ({row}, {col}) w macierzy o wymiarach {self.nrows()}x{self.ncols()}")

Przetestuj, czy działa: zapisz do pliku macierz jednostkową o wymiarach 10 na 10:

\[\begin{split}\begin{pmatrix} 1 & 0 & 0 & \cdots & 0 & 0 \\ 0 & 1 & 0 & \cdots & 0 & 0 \\ 0 & 0 & 1 & \cdots & 0 & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & \cdots & 1 & 0 \\ 0 & 0 & 0 & \cdots & 0 & 1 \end{pmatrix}\end{split}\]

Operacje elementarne na macierzach

Zaimplementujemy teraz kolejne funkcje do klasy macierzy, które wykorzystamy do rozwiązywania układów równań:

  • Matrix.swap_rows(row1, row2): zamienia miejscami wiersze o podanych indeksach.

  • Matrix.multiply(row, scalar): mnoży wiersz o podanym indeksie przez podaną wartość.

  • Matrix.add_row(dst_row, scalar, src_row): dodaje do wiersza o indeksie dst_row wiersz o indeksie src_row pomnożony przez scalar.

Zadanie 4: zamiana wierszy

Uzupełnij kod funkcji Matrix.swap_rows. Pamiętaj, że w Pythonie indeksy zaczynają się od zera. Przykładowo, jeżeli macierz m to:

\[\begin{split}\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}\end{split}\]

to po wykonaniu m.swap_rows(0, 1) macierz m powinna być równa:

\[\begin{split}\begin{pmatrix} 4 & 5 & 6 \\ 1 & 2 & 3 \end{pmatrix}\end{split}\]

Zadanie 5: pomnożenie wiersza

Uzupełnij kod funkcji Matrix.multiply_row, która pomnoży wiersz o danym indeksie (pierwszy wiersz ma index zero) przez podaną wartość. Przykładowo, jeżeli macierz m to:

\[\begin{split}\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}\end{split}\]

to po wykonaniu m.multiply_row(0, 2) macierz m powinna być równa:

\[\begin{split}\begin{pmatrix} 2 & 4 & 6 \\ 4 & 5 & 6 \end{pmatrix}\end{split}\]

Zadanie 6: dodawanie pomnożonego wiersza

Uzupełnij kod funkcji Matrix.add_row. Pamiętaj, że po jej wykonaniu jedynym zmienionym wierszem powinien być ten o indeksie podanym w pierwszym argumencie do funkcji; wszystkie pozostałe wiersze mają zostać, jak były. Przykładowo, jeśli macierz m jest równa:

\[\begin{split}\begin{pmatrix} 1 & 1 & 1 \\ 6 & 7 & 7 \\ 1 & 2 & 3 \end{pmatrix}\end{split}\]

To po wykonaniu m.add_row(1, -6.0, 0) powinna wynosić:

\[\begin{split}\begin{pmatrix} 1 & 1 & 1 \\ 0 & 1 & 1 \\ 1 & 2 & 3 \end{pmatrix}\end{split}\]

Wczytywanie i zapisywanie macierzy

Możesz teraz dokleić to zaklęcie do swojego pliku matrix.py, żeby odblokować swoim macierzom skilla zapisywania do pliku i wczytywania z niego:

def read_matrix_file(filename):
    number_of_columns = None
    number_of_rows = 0
    elements = []
    with open(filename) as opened_file:
        for lineno, line in enumerate(opened_file):
            line_elements = line.split()
            if not line_elements:
                continue
            number_of_rows += 1
            if number_of_columns is None:
                number_of_columns = len(line_elements)
            else:
                if len(line_elements) != number_of_columns:
                    raise ValueError(f"{filename}:{lineno}: zła liczba elementów"
                        f" - {len(line_elements)} zamiast {number_of_columns}")
            for element in line_elements:
                try:
                    elements.append(float(element))
                except ValueError:
                    raise ValueError(f"{filename}:{lineno}: niepoprawna liczba '{element}'")
    result = Matrix(number_of_rows, number_of_columns)
    elements.reverse()
    for r in range(number_of_rows):
        for c in range(number_of_columns):
            result.set(r, c, elements.pop())
    return result


def write_matrix_file(matrix, filename=None):
    sizes = [0] * matrix.ncols()
    for r in range(matrix.nrows()):
        for c in range(matrix.ncols()):
            sizes[c] = max(sizes[c], len(str(matrix.get(r, c))))
    if filename:
        opened_file = open(filename, "w")
    else:
        from sys import stdout
        opened_file = stdout
    for r in range(matrix.nrows()):
        for c in range(matrix.ncols()):
            if c != 0:
                opened_file.write("  ")
            opened_file.write(str(matrix.get(r, c)).rjust(sizes[c]))
        opened_file.write("\n")
    if filename:
        opened_file.close()

Instrukcje warunkowe

Do rozwiązania tego zadania będziesz potrzebować instrukcji warunkowych. W Pythonie instrukcja warunkowa składa się z:

  • Głównej części if warunek: wraz z instrukcjami wykonywanymi, gdy warunek jest spełniony (czyli gdy wartość logiczna wyrażenia warunek to True);

  • Opcjonalnie: części elif warunek: wraz z instrukcjami wykonywanymi: gdy:

    • żadna z poprzednich części nie była wykonana oraz…

    • warunek jest prawdziwy.

  • Opcjonalnie: części else: wraz z instrukcjami wykonywanymi, gdy żadna z wcześniejszych części nie została wykonana.

Przykładowo:

if ile == 0:
    print("Janek nie ma jabłek")
elif ile == 1:
    print(f"Janek ma {ile} jabłko")
elif 2 <= ile <= 4:
    print(f"Janek ma {ile} jabłka")
else:
    print(f"Janek ma {ile} jabłek")

W jaki sposób możemy uzyskać ten sam efekt, co dodając część else, ale używając elif?

Dlaczego potrzebujemy elif, skoro moglibyśmy (jak w C++) użyć else wraz z kolejnym if?

Ważne

Instrukcja warunkowa w Pythonie umożliwia wykonywanie różnego kodu, zależnie od prawdziwości warunków, które odwołują się do wartości zmiennych:

if jablka > pomarancze:
    print(f"Mam więcej jabłek niż pomarańczy")
elif jablka < pomarancze:
    print(f"Mam więcej pomarańczy niż jabłek")
else:
    print(f"Mam tyle samo jabłek, co pomarańczy")

Ilustracja składni instrukcji warunkowej wygenerowana przez Gemini

Zadanie 7: wypisywanie macierzy jako układu równań

Napisz funkcję Matrix.eqs_print(), która wypisze macierz w postaci układu równań.

Na przykład dla macierzy:

\[\begin{split}\begin{pmatrix} 1 & 2 & 3 & 4\\ 1 & -2 & 3 & -2 \\ 4 & 0 & 6 & 4 \end{pmatrix}\end{split}\]

Powinna wypisać:

x1 + 2*x2 + 3*x3 = 4
x1 - 2*x2 + 3*x3 = -2
4*x1 + 6*x3 = 4

Zwróć uwagę, że:

  • Kiedy współczynik przy jakiejś zmiennej wynosi 0, to w ogóle nic tam nie wypisujemy (\(x_2\) w trzecim równaniu).

  • Kiedy współczynnik przy jakiejś zmiennej wynosi 1, to pomijamy jego wartość (\(x_1\) w pierwszych dwóch równaniach).

  • Kiedy współczynnik jest ujemny, to piszemy \(-\) zamiast \(+\) (\(x_2\) w drugim równaniu).

Zadania domowe