배열

배열 에서 Python

96개의 연습 문제

배열 소개

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부터 시작_) 방향으로 쓸 수 있어요.

왼쪽에서의 인덱스 ⟹






0
👇🏾
1
👇🏾
2
👇🏾
3
👇🏾
4
👇🏾
5
👇🏾
P y t h o n
👆🏾
-6
👆🏾
-5
👆🏾
-4
👆🏾
-3
👆🏾
-2
👆🏾
-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 타입도 제공해요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요

배열 배우기

연습이 잠겨 있어요

배열 개념을 연습하려면 연습 문제 5개를 더 잠금 해제해요