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>)를 사용할 수 있어요.
배열은 동적 배열로 구현되어 있어요. Java의 Arraylist 타입과 비슷하죠. 배열은 길이를 미리 알 수 없는 (항목 수가 임의로 늘어나거나 줄어들 수 있는) 비슷한 데이터(문자열, 숫자, 세트 등)의 묶음을 저장할 때 가장 많이 쓰여요.
요소에 접근하거나, in으로 포함 여부를 확인하거나, 배열의 "오른쪽" 끝에 항목을 추가하는 것은 모두 매우 효율적이에요. 배열의 맨 앞에 추가하거나("왼쪽" 끝에 추가하는 것) 중간에 삽입하는 것은 훨씬 덜 효율적인데, 이런 연산은 순서를 유지하려고 요소를 밀어내야 하기 때문이에요. 양쪽 끝에서 메모리 효율적으로 appends/pops를 지원하는 비슷한 자료 구조로는 collections.deque가 있어요. 어느 방향이든 거의 같은 O(1) 성능을 내죠.
배열은 변경 가능하고 임의의 Python 객체에 대한 참조를 담을 수 있기 때문에, 겉으로 보이는 길이가 같아도 array.array나 (불변인) tuple보다 메모리를 더 많이 차지해요. 그럼에도 배열은 매우 유연하고 유용한 자료 구조이고, Python의 많은 내장 메서드와 연산이 결과로 배열을 만들어 내요.
list는 대괄호 []와 요소 사이의 쉼표를 사용해 _리터럴_로 선언할 수 있어요:
>>> 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]
list() 생성자에 문자열이나 딕셔너리를 넘기면 결과가 의외일 수 있어요:
# 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']
lists 생성자는 이터러블(또는 아무것도)만 인자로 받기 때문에, 이터러블이 아닌 객체를 넘기면 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(0부터 시작) 또는 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의 변수는 _바탕이 되는 객체_를 가리키는 _레이블_이라는 걸 기억해요. 여기에 배열은 _컨테이너 객체_라는 층을 하나 더 얹어요. 담고 있는 항목에 대한 객체 _참조_를 들고 있죠. 제대로 다루지 않으면 배열을 쓸 때 여러 문제가 생길 수 있어요.
list 객체에 새 변수 _이름_을 붙여도 list 객체와 그 요소가 복사되지 않아요.
새 이름으로 list의 요소를 바꾸면 _원래 것에도 영향_을 줘요.
list.copy()나 슬라이스로 shallow_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"]
이런 참조 문제는 중첩되거나 곱해진 배열을 다룰 때 더 심해져요(다음 예제는 2013년 Ned Batchelder의 훌륭한 블로그 글 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 타입도 제공해요.