Масив (list) - це змінювана колекція елементів, розташованих послідовно.
Як і більшість колекцій (як-от вбудовані tuple, dict і set), масиви можуть містити посилання на будь-який тип даних (або одразу на кілька), зокрема на інші масиви.
Як і в будь-якій послідовності, до елементів можна звертатися за 0-based index зліва та -1-based index справа.
Масиви можна копіювати повністю або частково за допомогою синтаксису зрізів або <list>.copy().
Масиви підтримують операції над послідовностями - як спільні, так і призначені для змінюваних послідовностей, зокрема min()/max(), <list>.index(), .append() і .reverse().
Елементи масиву можна перебирати за допомогою конструкції for item in <list>. Конструкцію for index, item in enumerate(<list>) можна використати, коли потрібні як індекс елемента, так і його значення.
Масиви реалізовано як динамічні масиви, подібні до типу Arraylist у Java, і найчастіше їх використовують для зберігання груп однотипних даних (рядків тексту (англ. string), чисел, множин тощо) невідомої довжини (кількість записів може довільно зростати або зменшуватися).
Доступ до елементів, перевірка належності за допомогою in і додавання елементів у «правий» бік масиву - усе це дуже ефективно.
Додавання на початок (додавання в «лівий» бік) або вставлення в середину масиву є значно менш ефективним, бо ці операції вимагають зсування елементів, щоб зберегти їхню послідовність.
Для схожої структури даних, яка підтримує економні за памʼяттю appends/pops з обох боків, звернімо увагу на collections.deque, який має приблизно однакову продуктивність O(1) в обидва боки.
Оскільки масиви змінювані й можуть містити посилання на довільні обʼєкти Python, вони також займають більше памʼяті, ніж array.array або tuple (який є незмінним) тієї самої видимої довжини.
Попри це, масиви - надзвичайно гнучка й корисна структура даних, і багато вбудованих методів і операцій Python повертають масиви як свої вихідні дані.
Масив можна оголосити як літерал за допомогою квадратних дужок [] і ком між елементами:
>>> no_elements = []
>>> no_elements
[]
>>> one_element = ["Guava"]
>>> one_element
['Guava']
>>> elements_separated_with_commas = ["Parrot", "Bird", 334782]
>>> elements_separated_with_commas
['Parrot', 'Bird', 334782]
Для кращої читабельності можна використовувати перенесення рядків, коли в масиві багато елементів або вкладених структур даних:
>>> lots_of_entries = [
... "Rose",
... "Sunflower",
... "Poppy",
... "Pansy",
... "Tulip",
... "Fuchsia",
... "Cyclamen",
... "Lavender"
... ]
>>> lots_of_entries
['Rose', 'Sunflower', 'Poppy', 'Pansy', 'Tulip', 'Fuchsia', 'Cyclamen', 'Lavender']
# Each data structure is on its own line to help clarify what they are.
>>> nested_data_structures = [
... {"fish": "gold", "monkey": "brown", "parrot": "grey"},
... ("fish", "mammal", "bird"),
... ['water', 'jungle', 'sky']
... ]
>>> nested_data_structures
[{'fish': 'gold', 'monkey': 'brown', 'parrot': 'grey'}, ('fish', 'mammal', 'bird'), ['water', 'jungle', 'sky']]
Конструктор list() можна викликати без аргументів або з ітерабельним обʼєктом як аргументом.
Конструктор по черзі перебирає елементи ітерабельного обʼєкта й додає їх до масиву в порядку слідування:
>>> no_elements = list()
>>> no_elements
[]
# The tuple is unpacked and each element is added.
>>> multiple_elements_from_tuple = list(("Parrot", "Bird", 334782))
>>> multiple_elements_from_tuple
['Parrot', 'Bird', 334782]
# The set is unpacked and each element is added.
>>> multiple_elements_from_set = list({2, 3, 5, 7, 11})
>>> multiple_elements_from_set
[2, 3, 5, 7, 11]
Результат використання конструктора масиву з рядком тексту або словником може здивувати:
# String elements (Unicode code points) are iterated through and added *individually*.
>>> multiple_elements_string = list("Timbuktu")
>>> multiple_elements_string
['T', 'i', 'm', 'b', 'u', 'k', 't', 'u']
# Unicode separators and positioning code points are also added *individually*.
>>> multiple_code_points_string = list('अभ्यास')
>>> multiple_code_points_string
['अ', 'भ', '्', 'य', 'ा', 'स']
# The iteration default for dictionaries is over the keys, so only key data is inserted into the list.
>>> source_data = {"fish": "gold", "monkey": "brown"}
>>> list(source_data)
['fish', 'monkey']
Оскільки конструктор list() приймає як аргументи лише ітерабельні обʼєкти (або нічого), обʼєкти, які не є ітерабельними, спричинять помилку TypeError. Тому створити масив з одного елемента набагато простіше через літерал.
# Numbers are not iterable, and so attempting to create a list with a number passed to the constructor fails.
>>> one_element = list(16)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
# Tuples *are* iterable, so passing a one-element tuple to the constructor does work, but it's awkward
>>> one_element_from_iterable = list((16,))
>>> one_element_from_iterable
[16]
До елементів усередині масивів (як і до елементів в інших типах послідовностей, як-от str і tuple) можна звертатися за допомогою запису в дужках.
Індекси можуть іти від left --> right (починаючи з нуля) або від right --> left (починаючи з -1).
| індекс зліва ⟹ |
|
⟸ індекс справа |
>>> breakfast_foods = ["Oatmeal", "Fruit Salad", "Eggs", "Toast"]
# Oatmeal is at index 0 or index -4.
>>> breakfast_foods[0]
'Oatmeal'
>>> breakfast_foods[-4]
'Oatmeal'
# Eggs are at index -2 or 2
>>> breakfast_foods[-2]
'Eggs'
>>> breakfast_foods[2]
'Eggs'
# Toast is at -1
>>> breakfast_foods[-1]
'Toast'
До частини масиву можна звертатися через синтаксис зрізів (<list>[<start>:<stop>]).
Кажуть, що зріз охоплює послідовність елементів на позиціях index, для яких виконується start <= index < stop.
Зрізання повертає копію «вирізаних» елементів і не змінює вихідний list.
У зрізі також можна використати параметр step (<list>[<start>:<stop>:<step>]), щоб «пропускати» або фільтрувати повернені елементи (наприклад, step зі значенням 2 вибере кожен другий елемент у цій частині):
>>> colors = ["Red", "Purple", "Green", "Yellow", "Orange", "Pink", "Blue", "Grey"]
# If there is no step parameter, the step is assumed to be 1.
>>> middle_colors = colors[2:6]
>>> middle_colors
['Green', 'Yellow', 'Orange', 'Pink']
# If the start or stop parameters are omitted, the slice will
# start at index zero, and will stop at the end of the list.
>>> primary_colors = colors[::3]
>>> primary_colors
['Red', 'Yellow', 'Blue']
Масиви надають ітератор, і їх можна перебирати в циклі так само, як інші типи послідовностей, за допомогою або for item in <list>, або for index, item in enumerate(<list>):
# Make a list, and then loop through it to print out the elements
>>> colors = ["Orange", "Green", "Grey", "Blue"]
>>> for item in colors:
... print(item)
Orange
Green
Grey
Blue
# Print the same list, but with the indexes of the colors included
>>> colors = ["Orange", "Green", "Grey", "Blue"]
>>> for index, item in enumerate(colors):
... print(item, ":", index)
Orange : 0
Green : 1
Grey : 2
Blue : 3
# Start with a list of numbers and then loop through and print out their cubes.
>>> numbers_to_cube = [5, 13, 12, 16]
>>> for number in numbers_to_cube:
... print(number**3)
125
2197
1728
4096
Один із поширених способів скласти масив значень - використати <list>.append() у циклі:
>>> cubes_to_1000 = []
>>> for number in range(11):
... cubes_to_1000.append(number**3)
>>> cubes_to_1000
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]
Масиви також можна поєднувати різними способами:
# Using the plus + operator unpacks each list and creates a new list, but it is not efficient.
>>> new_via_concatenate = ["George", 5] + ["cat", "Tabby"]
>>> new_via_concatenate
['George', 5, 'cat', 'Tabby']
# Likewise, using the multiplication operator * is the equivalent of using + n times.
>>> first_group = ["cat", "dog", "elephant"]
>>> multiplied_group = first_group * 3
>>> multiplied_group
['cat', 'dog', 'elephant', 'cat', 'dog', 'elephant', 'cat', 'dog', 'elephant']
# Another method for combining 2 lists is to use slice assignment or a loop-append.
# This assigns the second list to index 0 in the first list.
>>> first_one = ["cat", "Tabby"]
>>> second_one = ["George", 5]
>>> first_one[0:0] = second_one
>>> first_one
['George', 5, 'cat', 'Tabby']
# This loops through the first list and appends its items to the end of the second list.
>>> first_one = ["cat", "Tabby"]
>>> second_one = ["George", 5]
>>> for item in first_one:
... second_one.append(item)
>>> second_one
['George', 5, 'cat', 'Tabby']
Пригадаймо, що змінні в Python - це мітки, які вказують на обʼєкти в памʼяті.
lists додають ще один рівень як обʼєкти-контейнери: вони зберігають посилання на обʼєкти для зібраних елементів.
Якщо не поводитися з ними належно, це може призвести до багатьох потенційних проблем під час роботи з масивами.
Присвоєння обʼєкта list новій назві змінної не копіює ні обʼєкт list, ні його елементи.
Будь-яка зміна елементів масиву під новою назвою впливає й на оригінал.
Створення shallow_copy за допомогою list.copy() або зрізу допоможе уникнути цієї проблеми з посиланнями першого рівня.
shallow_copy створить новий обʼєкт list, але не створить нових обʼєктів для вміщених елементів масиву. Такого копіювання зазвичай досить, щоб незалежно додавати чи вилучати елементи в двох обʼєктах list і фактично мати два «окремих» масиви.
>>> actual_names = ["Tony", "Natasha", "Thor", "Bruce"]
# Assigning a new variable name does not make a copy of the container or its data.
>>> same_list = actual_names
# Altering the list via the new name is the same as altering the list via the old name.
>>> same_list.append("Clarke")
["Tony", "Natasha", "Thor", "Bruce", "Clarke"]
>>> actual_names
["Tony", "Natasha", "Thor", "Bruce", "Clarke"]
# Likewise, altering the data in the list via the original name will also alter the data under the new name.
>>> actual_names[0] = "Wanda"
['Wanda', 'Natasha', 'Thor', 'Bruce', 'Clarke']
# If you copy the list, there will be two separate list objects which can be changed independently.
>>> copied_list = actual_names.copy()
>>> copied_list[0] = "Tony"
>>> actual_names
['Wanda', 'Natasha', 'Thor', 'Bruce', 'Clarke']
>>> copied_list
["Tony", "Natasha", "Thor", "Bruce", "Clarke"]
Ця проблема з посиланнями загострюється, коли ми працюємо з вкладеними або помноженими масивами (наведені далі приклади взято з чудового допису Ned Batchelder 2013 року Names and values: making a game board):
from pprint import pprint
# This will produce a game grid that is 8x8, pre-populated with zeros.
>>> game_grid = [[0]*8]*8
>>> pprint(game_grid)
[[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]
# An attempt to put a "X" in the bottom right corner.
>>> game_grid[7][7] = "X"
# This attempt doesn't work because all the rows are referencing the same underlying list object.
>>> pprint(game_grid)
[[0, 0, 0, 0, 0, 0, 0, 'X'],
[0, 0, 0, 0, 0, 0, 0, 'X'],
[0, 0, 0, 0, 0, 0, 0, 'X'],
[0, 0, 0, 0, 0, 0, 0, 'X'],
[0, 0, 0, 0, 0, 0, 0, 'X'],
[0, 0, 0, 0, 0, 0, 0, 'X'],
[0, 0, 0, 0, 0, 0, 0, 'X'],
[0, 0, 0, 0, 0, 0, 0, 'X']]
Але в цьому випадку shallow_copy цілком достатньо, щоб отримати бажану поведінку:
from pprint import pprint
# This loop will safely produce a game grid that is 8x8, pre-populated with zeros
>>> game_grid = []
>>> filled_row = [0] * 8
>>> for row in range(8):
... game_grid.append(filled_row.copy()) # This is making a new shallow copy of the inner list object each iteration.
>>> pprint(game_grid)
[[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]
# An attempt to put a "X" in the bottom right corner.
>>> game_grid[7][7] = "X"
# The game grid now works the way we expect it to!
>>> pprint(game_grid)
[[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 'X']]
Як уже згадувалося, масиви - це контейнери посилань, тож тут є другий рівень потенційних ускладнень.
Якщо масив містить змінні, обʼєкти або вкладені структури даних, ці посилання другого рівня не буде скопійовано за допомогою shallow_copy чи зрізу.
Тоді зміна обʼєктів у памʼяті вплине на всі без винятку копії, оскільки кожен обʼєкт list містить лише посилання на вміщені елементи.
from pprint import pprint
>>> pprint(game_grid)
[[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 'X']]
# We'd like a new board, so we make a shallow copy.
>>> new_game_grid = game_grid.copy()
# But a shallow copy doesn't copy the contained references or objects.
>>> new_game_grid[0][0] = 'X'
# So changing the items in the copy also changes the originals items.
>>> pprint(game_grid)
[['X', 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 'X']]
Масиви часто використовують як стеки та черги, хоча їхня внутрішня реалізація робить додавання на початок і вставлення повільними.
Модуль collections пропонує варіант deque, оптимізований для швидких додавань і вилучень з обох кінців, реалізований як двобічно звʼязаний список.
Вкладені масиви також використовують для моделювання невеликих матриць, хоча бібліотеки Numpy і Pandas значно надійніші для ефективної роботи з матрицями та табличними даними.
Модуль collections також надає тип UserList, який можна налаштувати під специфічні потреби роботи з масивами.