Теоретичні відомості#

Одна з чудових особливостей базових типів мови Python полягає в тому, що вони підтримують можливість створення вкладених конструкцій довільної глибини і в будь-яких комбінаціях. Одне з очевидних застосувань цієї особливості — уявлення матриць, або “багатовимірних масивів” в мові Python. Робиться це за допомогою списку, який містить вкладені списки:

M = [[1, 2, 3], # Матриця 3 x 3 у вигляді вкладених списків
     [4, 5, 6], # Вираз у квадратних дужках може
     [7, 8, 9]] # займати кілька рядків
M
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Тут реалізовано список, що складається із трьох інших списків. У результаті була отримана матриця чисел 3 x 3. Звертатися до такої структури можна різними способами:

M[1] # Отримати рядок 2
[4, 5, 6]
M[1][2] # Отримати рядок 2, а потім елемент 3 в цьому рядку
6

Перша операція в цьому прикладі повертає другий рядок цілком, а друга — третій елемент в цьому рядку. З’єднання операцій індексування дозволяє все далі і далі занурюватися вглиб вкладеної структури об’єктів.

Така організація матриць цілком придатна для вирішення невеликих завдань, але для реалізації більш складних програм цифрової обробки інформації бажано використовувати спеціалізовані розширення, наприклад NumPy. Такого роду інструменти дозволяють зберігати і обробляти матриці набагато ефективніше, ніж така структура, яка реалізована у вигляді вкладених списків. Як вже говорилося, розширення NumPy перетворює Python в вільний і більш потужний еквівалент системи MatLab.

Генератори матриць#

Основний спосіб реалізації матриць (вони ж — багатовимірні масиви) в мові Python полягає в використанні вкладених списків. У наступному прикладі визначаються дві матриці 3x3 у вигляді вкладених списків:

M = [[1, 2, 3], 
     [4, 5, 6], 
     [7, 8, 9]]

N = [[2, 2, 2], 
     [3, 3, 3], 
     [4, 4, 4]]

При такій організації завжди можна використовувати звичайну операцію індексування для звернення до рядків і елементів усередині рядків:

M[1]
[4, 5, 6]
M[1][2] 
6

Генератори списків є потужним засобом обробки таких структур даних, тому що вони дозволяють автоматично сканувати рядки і стовпці матриць. Наприклад, незважаючи на те, що при такій організації матриці зберігаються у вигляді списку рядків, можна легко добути другий стовпець, просто обходячи рядки матриці і вибираючи елементи з необхідного стовпця або виконуючи обхід необхідних позицій в рядках: (див.[1] стор.585).

[row[1] for row in M]
[2, 5, 8]
[M[row][1] for row in (0,1,2)]
[2, 5, 8]

Використовуючи позиції, так само легко можна добути елементи, які лежать на діагоналі. У наступному прикладі використовується функція range — вона створює список зсувів, який потім використовується для індексування рядків і стовпців одним і тим же значенням. В результаті спочатку вибирається M[0][0], потім M[1][1] і так далі (тут мається на увазі, що матриця має однакове число рядків і стовпців):

[M[i][i] for i in range(len(M))]
[1, 5, 9]

Генератори списків можна використовувати для об’єднання кількох матриць. Перший приклад нижче створює простий список, що містить результати множення відповідних елементів двох матриць, а другий створює структуру вкладених списків, з тими ж самими значеннями:

[M[row][col] * N[row][col] for row in range(3) for col in range(3)]
[2, 4, 6, 12, 15, 18, 28, 32, 36]
[[M[row][col] * N[row][col] for col in range(3)] for row in range(3)]
[[2, 4, 6], [12, 15, 18], [28, 32, 36]]

В останньому виразі ітерації по рядках виконуються в зовнішньому циклі: для кожного рядка запускається ітерація по стовпцях, яка створює один рядок в матриці з результатами. Цей вислів еквівалентний наступному фрагменту:

res = [] 
for row in range(3): 
    tmp = [] 
    for col in range(3): 
        tmp.append(M[row][col] * N[row][col]) 
    res.append(tmp) 
res
[[2, 4, 6], [12, 15, 18], [28, 32, 36]]

На відміну від цього фрагмента, версія на базі генератора списків вміщується в єдиний рядок і, ймовірно, працює значно швидше в разі великих матриць, але, правда, складніше для сприйняття. Тому початківцям освоювати мову Python рекомендується в більшості випадків використовувати прості цикли for і функцію map, а генератори — в окремих випадках (якщо вони виходять не надто складними). Тут також діє правило “чим простіше, тим краще”: лаконічність програмного коду — набагато менш важлива мета, ніж його читабельність.

З іншого боку, ускладнення програмного коду забезпечує більш високу його продуктивність: проведені тести свідчать, що функція map працює практично в два рази швидше, ніж еквівалентні цикли for, а генератори списків зазвичай трохи швидше, ніж функція map. Цю різницю в швидкості виконання обумовлено тим фактом, що функція map і генератори списків реалізовані на мові C, що забезпечує більш високу швидкість, ніж виконання циклів for всередині віртуальної машини Python.

Застосування циклів for робить логіку програми більш явною, тому можна рекомендувати використовувати їх для забезпечення більшої простоти. Однак функція map і генератори списків варті того, щоб знати і застосовувати їх для реалізації простих ітерацій, а також у випадках, коли швидкість роботи програми має критично важливе значення. Крім того, функція map і генератори списків є вирази і синтаксично можуть перебувати там, де неприпустимо використовувати інструкцію for, наприклад, в тілі lambda-виразів , тобто треба намагатися писати прості функції map і генератори списків, а в більш складних випадках використовувати повні інструкції.

Модуль array для одновимірних масивів#

Модуль array містить визначення типу послідовності array.array, здатної зберігати числа або символи досить економним способом. Цей тип даних нагадує списки, за винятком того, що об’єкти цього типу можуть зберігати лише елементи певного типу, який визначається на етапі його створення, тому, на відміну від списків, вони не можуть одночасно зберігати об’єкти різних типів. Масиви підтримують всі спискові методи (індексація, зрізи, множення, ітерації), і інші методи. Масиви використовуються, коли потрібно досягти високої швидкості роботи. В інших випадках масиви можна замінити іншими типами даних: списками, кортежами, рядками.

Але, якщо потрібна максимально ефективна робота з масивами, слід скористатися типом array з пакета NumPy.

Розмір і тип елемента в масиві визначається при його створенні і може набувати таких значень:

Код типу

Тип в Python

Розмір в байтах

‘b’

int

1

‘B’

int

1

‘h’

int

2

‘H’

int

2

‘i’

int

2

‘I’

int

2

‘l’

int

4

‘L’

int

4

‘q’

int

8

‘Q’

int

8

‘f’

float

4

‘d’

float

8

array.array(typecode[,initializer]), де typecode — тип елементів масиву, initializer — рядок або список значень, які використовуються для ініціалізації значень елементів масиву.

Методи масивів (array) в Python#

array.typecode — typecode-символ, використаний при створенні масиву.

array.itemsize — розмір в байтах одного елемента в масиві.

array.append(х) — додавання елемента в кінець масиву.

array.buffer_info() — кортеж (комірка пам’яті, довжина). Корисно для низькорівневих операцій.

array.count(х) — повертає кількість входжень х в масив.

array.extend(iter) — додавання елементів із об’єкта в масив.

array.frombytes(b) — робить масив типу array із масиву байт. Кількість байт повинна бути кратна розміру одного елемента в масиві.

array.fromlist(список) — додавання елементів зі списку.

array.index(х) — номер першого входження x в масив.

array.insert(n, х) — включити новий пункт зі значенням х в масиві перед номером n. Від’ємні значення розглядаються відносно кінця масиву.

array.pop(i) − видаляє i-ий елемент із масиву і повертає його. За замовчуванням видаляється останній елемент.

array.remove(х) — видалити перше входження х із масиву.

array.reverse() — зворотний порядок елементів в масиві.

array.tobytes() — перетворення до байтового рядка.

array.tolist() — перетворення масиву в список.

Приклади:

# Ініцалізація, змінення, додавання елементів зі списку, 
# додавання і знаходження максимального елемента в масиві.
import array


a = array.array('i', [1, 2, 3, 4, 5])
b = array.array(a.typecode, (2 * x for x in a))
lst = [20, 40, 60, 80, 100]
b.fromlist(lst)
print(b)

array.array('i', [2, 4, 6, 8, 10, 20, 40, 60, 80, 100])
print(b[4] + b[6])
print(max(b))
array('i', [2, 4, 6, 8, 10, 20, 40, 60, 80, 100])
50
100
# Додавання елементів в масив
import array


a = array.array('i', [1, 2, 3, 4, 5])
a.extend([100, 200, 300])
print(a)
array('i', [1, 2, 3, 4, 5, 100, 200, 300])
# Перетворення масиву в список
a = array.array('i', [1, 2, 3, 4, 5, 100])
a.tolist()
[1, 2, 3, 4, 5, 100]

Модуль NumPy для матриць#

NumPy — це бібліотека мови Python, що додає підтримку великих багатовимірних масивів і матриць, разом з великою бібліотекою високорівневих (і дуже швидких) математичних функцій для операцій з цими масивами.

Основним об’єктом NumPy є однорідний багатовимірний масив (в numpy називається numpy.ndarray). Це багатовимірний масив елементів (зазвичай чисел), одного типу.

Найбільш важливі атрибути об’єктів ndarray:

ndarray.ndim — число вимірювань (частіше їх називають “осі”) масиву.

ndarray.shape — розміри масиву, його форма. Це кортеж натуральних чисел, що показує довжину масиву по кожній осі. Для матриці із n рядків і m стовпців, shape буде (n, m). Число елементів кортежу shape дорівнює ndim.

ndarray.size — кількість елементів масиву. Очевидно, дорівнює добутку всіх елементів атрибута shape.

ndarray.dtype − об’єкт, що описує тип елементів масиву. Можна визначити dtype, використовуючи стандартні типи даних Python. NumPy тут надає цілий букет можливостей, як вбудованих, наприклад: bool_, character, int8, int16, int32, int64, float8, float16, float32, float64, complex64, object_, так і можливість визначити власні типи даних, в тому числі і складні.

ndarray.itemsize − розмір кожного елемента масиву в байтах.

Створення масивів#

У NumPy існує багато способів створити масив. Один із найбільш простих — створити масив із звичайних списків або кортежів Python, використовуючи функцію numpy.array. Функція array трансформує вкладені послідовності в багатовимірні масиви. Тип елементів масиву залежить від типу елементів вихідної послідовності (але можна і перевизначити його в момент створення).

import numpy as np


b = np.array([[1.5, 2, 3], [4, 5, 6]])
print(b)
[[1.5 2.  3. ]
 [4.  5.  6. ]]

Є кілька функцій для того, щоб створювати масиви з якимось вихідним вмістом (за замовчуванням тип створюваного масиву — float64).

Функція zeros створює масив із нулів, а функція ones — масив із одиниць. Обидві функції приймають кортеж з розмірами, і аргумент dtype.

Функція eye створює одиничну матрицю (двовимірний масив).

Функція empty створює масив без його заповнення. Початковий вміст випадковий і залежить від стану пам’яті на момент створення масиву (тобто від того сміття, що в ній зберігається).

Для створення послідовностей чисел, в NumPy є функція arange, аналогічна вбудованої в Python range, тільки замість списків вона повертає масиви, і приймає не тільки цілі значення. Взагалі, при використанні arange з аргументами типу float, складно бути впевненим в тому, скільки елементів буде отримано (через обмеження точності чисел з плаваючою комою). Тому, в таких випадках зазвичай краще використовувати функцію linspace, яка замість кроку в якості одного із аргументів приймає число, що дорівнює кількості потрібних елементів.

Приклад:

print(np.linspace(0, 2, 9)) # 9 чисел від 0 до 2 включно

# fromfunction(): застосовує функцію до всіх комбінацій індексів
def f1(i, j):
    return 3 * i + j

print(np.fromfunction(f1, (3, 4)))
print(np.fromfunction(f1, (3, 3)))
[0.   0.25 0.5  0.75 1.   1.25 1.5  1.75 2.  ]
[[0. 1. 2. 3.]
 [3. 4. 5. 6.]
 [6. 7. 8. 9.]]
[[0. 1. 2.]
 [3. 4. 5.]
 [6. 7. 8.]]

Базові операції#

Математичні операції над масивами виконуються поелементно. Створюється новий масив, який заповнюється результатами дій оператора.

import numpy as np
a = np.array([20, 30, 40, 50])
b = np.arange(4)
print(a + b)
print(a - b)
print(a * b)
[20 31 42 53]
[20 29 38 47]
[  0  30  80 150]

Для цього, природно, масиви повинні бути однакових розмірів. Також можна робити математичні операції між масивом і числом. У цьому випадку операції виконуються з кожним елементом масиву. Багато унарних операцій, таких як, наприклад, обчислення суми всіх елементів масиву, представлені також і у вигляді методів класу ndarray. За замовчуванням, ці операції застосовуються до масиву, як би він був списком чисел, незалежно від його форми. Однак, вказавши параметр axis, можна застосувати операцію для зазначеної осі масиву:

a = np.array([[1, 2, 3], [4, 5, 6]])
print(a.min(axis=0)) # Найменше число в кожному стовпці
print(a.min(axis=1)) # Найменше число в кожному рядку
[1 2 3]
[1 4]

Індекси, зрізи, ітерації#

Одновимірні масиви здійснюють операції індексування, зрізів і ітерацій дуже схожим чином з звичайними списками і іншими послідовностями Python (хіба що видаляти за допомогою зрізів не можна).

Приклад:

a = np.arange(10) ** 3
print(a)
print(a[1])
print(a[3:7])

a[3:7] = 8
print(a)
print(a[::-1])
[  0   1   8  27  64 125 216 343 512 729]
1
[ 27  64 125 216]
[  0   1   8   8   8   8   8 343 512 729]
[729 512 343   8   8   8   8   8   1   0]

У багатовимірних масивів на кожну вісь припадає один індекс. Індекси передаються у вигляді послідовності чисел, розділених комами, тобто кортежами:

b = np.array([[0,1,2,3],
    [10,11,12,13],
    [20,21,22,23],
    [30,31,32,33],
    [40,41,42,43]])

print(b[2, 3]) # Другий рядок, третій стовпець

print(b[(2,3)])
print(b[2][3]) # Можна і так

print(b[:, 2]) # Третій стовпець
print(b[:2])   # Перші два рядки

print(b[1:3, ::]) # Другий і третій рядки
23
23
23
[ 2 12 22 32 42]
[[ 0  1  2  3]
 [10 11 12 13]]
[[10 11 12 13]
 [20 21 22 23]]

Маніпуляції з формою#

Форма масиву (shape) визначається числом елементів вздовж кожної осі:

a = np.array([
    [[0, 1, 2], [10, 12, 13]],
    [[100, 101, 102], [110, 112, 113]]
])
print(a.shape)
(2, 2, 3)

Форма масиву може бути змінена за допомогою різних команд:

a.ravel() # робить масив пласким 
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
a.shape = (6,2)# змінаформи
print(a)
[[ 0  1]
 [ 2  3]
 [ 4  5]
 [ 6  7]
 [ 8  9]
 [10 11]]
a.transpose() # транспонування
array([[ 0,  6],
       [ 1,  7],
       [ 2,  8],
       [ 3,  9],
       [ 4, 10],
       [ 5, 11]])
a.reshape((3,4)) # зміна форми
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

Об’єднання масивів#

Кілька масивів можуть бути об’єднані разом уздовж різних осей за допомогою функцій hstack і vstack.

hstack об’єднує масиви по горизонталі осях, vstack — по вертикалі:

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(np.vstack((a,b)))
print(np.hstack((a,b)))
[[1 2]
 [3 4]
 [5 6]
 [7 8]]
[[1 2 5 6]
 [3 4 7 8]]

Функція column_stack об’єднує одновимірні масиви в якості стовпців двовимірного масиву:

np.column_stack((a,b))
array([[1, 2, 5, 6],
       [3, 4, 7, 8]])

Аналогічно для рядків є функція row_stack:

np.row_stack((a,b))
array([[1, 2],
       [3, 4],
       [5, 6],
       [7, 8]])

Розбиття масиву#

Використовуючи hsplit, можна розбити масив вздовж горизонтальної осі, вказавши або кількість масивів, що повертаються однакової форми, або номера стовпців, після яких масив розрізається “ножицями”:

a = np.arange(12).reshape((2,6))
print(a)
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]]
np.hsplit(a,3) # розбити на 3 частини
[array([[0, 1],
        [6, 7]]),
 array([[2, 3],
        [8, 9]]),
 array([[ 4,  5],
        [10, 11]])]
np.hsplit(a, (3, 4)) # розрізати a після третього і четвертого стовпця
[array([[ 0,  1],
        [ 2,  3],
        [ 4,  5],
        [ 6,  7],
        [ 8,  9],
        [10, 11]]),
 array([], shape=(6, 0), dtype=int32),
 array([], shape=(6, 0), dtype=int32)]

Функція vsplit розбиває масив вздовж вертикальної осі, а array_split дозволяє вказати осі, уздовж яких відбудеться розбиття.

numpy.random#

На додаток слід зупинитися на темі створення масивів з випадкових елементів і роботі з випадковими елементами в NumPy. Для створення масивів з випадковими елементами служить модуль numpy.random.

Створення масивів#

Найпростіший спосіб задати масив з випадковими елементами – викори-стовувати функцію sample (або random, або random_sample, або ranf — це одна і та ж функція).

import numpy as np  # імпортувати numpy
np.random.sample()
0.46978031900261885
np.random.sample(3)
array([0.65261116, 0.40181207, 0.04443694])
np.random.sample((2,3))
array([[0.68945007, 0.72901298, 0.26720436],
       [0.10341684, 0.03208886, 0.88060434]])

Без аргументів повертає просто число в проміжку (0,1), з одним цілим числом — одновимірний масив, з кортежем — масив з розмірами, зазначеними в кортежі (всі числа — з проміжку (0,1)). За допомогою функції randint або random_integers можна створити масив з цілих чисел. Аргументи: low, high, size: від якого до якого числа (randint не включає в себе це число, а random_integers включає), і size — розміри масиву.

np.random.randint(0, 3, 10)
array([0, 2, 0, 1, 1, 2, 2, 2, 2, 0])
np.random.randint(0, 3, (2, 10))
array([[1, 1, 0, 2, 1, 1, 0, 0, 2, 0],
       [2, 1, 2, 0, 0, 0, 1, 0, 2, 2]])

Вибір і перемішування#

Перемішати NumPy масив можна за допомогою функції shuffle:

a = np.arange(10)
print(a)
[0 1 2 3 4 5 6 7 8 9]
np.random.shuffle(a)
print(a)
[7 0 1 6 8 4 2 3 5 9]

Також можна перемішати масив за допомогою функції permutation (вона, на відміну від shuffle, повертає перемішаний масив). Також вона, викликана з одним аргументом, повертає перемішану послідовність від 0 до n.

Приклад:

np.random.permutation(10)
array([2, 9, 8, 6, 1, 3, 5, 4, 7, 0])

Зробити випадкову вибірку з масиву можна за допомогою функції choice.

numpy.random.choice(a, size=None, replace=True, p=None)

  • a − одновимірний масив або число. Якщо масив, буде здійснюватися вибірка з нього. Якщо число, то вибірка буде вироблятися з np.arange(a).

  • size − розмірності масиву. Якщо None, повертається одне значення.

  • replace − якщо True, то одне значення може вибиратися більш ніж один раз.

  • p − ймовірності. Це означає, що елементи можна вибирати з нерівними можливостями. Якщо не задані, використовується рівномірний розподіл.

a = np.arange(10)
print(a)
[0 1 2 3 4 5 6 7 8 9]
np.random.choice(a, 10, p=[0.5, 0.25, 0.25, 0, 0, 0, 0, 0, 0, 0])
array([0, 0, 0, 0, 0, 2, 0, 2, 0, 0])