상위 목록: 하위 목록: 작성 날짜: 읽는 데 10 분 소요

목록(List)

Python에서는 목록(List)을 이용하여 다양한 연산이 가능합니다.

목록은 여러 개의 값을 순서대로 담아 두는 자료형입니다. 학생 점수 30개를 변수 30개에 따로 저장하는 대신, 목록 하나에 담아 두면 몇 번째 값이라는 위치로 꺼내 쓰거나 한꺼번에 반복 처리할 수 있습니다.

데이터 형식과는 무관하게 저장할 수 있으며 List 안에 또 다른 List를 포함시킬 수 있습니다.

List는 대괄호([])를 사용하며 순서를 중요시합니다. 또한 +, * 연산 시 원소에 대한 값이 아닌 List 자체에 대한 연산을 실시합니다.

즉, [1, 2] * 2는 각 원소에 2를 곱한 [2, 4]가 아닌, 목록 자체를 반복한 [1, 2, 1, 2]가 됩니다.


목록의 성질은 다음 네 가지로 정리할 수 있습니다. 앞으로 배울 튜플(Tuple), 사전(Dictionary), 집합(Set)도 이 네 가지 기준으로 비교하게 됩니다.

  • 변경 가능(Mutable) : 만든 뒤에도 원소를 바꾸거나 추가·삭제할 수 있습니다.
  • 순서가 있음 : 원소는 넣은 순서대로 0, 1, 2, … 번 위치(index)를 가집니다.
  • 중복 허용 : 같은 값이 여러 번 들어 있어도 됩니다.
  • 해시를 사용하지 않음 : 값을 찾을 때 앞에서부터 차례대로 비교합니다. (집합·사전과의 차이점이며, 9강과 13강에서 다시 다룹니다.)

목록은 내부적으로 각 원소(객체)를 가리키는 참조를 연속된 칸에 나란히 저장합니다. 그래서 목록[5]처럼 위치를 알고 있으면 목록이 아무리 길어도 해당 칸으로 바로 이동하여 일정한 시간에 값을 꺼낼 수 있습니다.

반면 목록은 값 자체를 담는 것이 아니라 값을 가리키는 참조를 담기 때문에, 같은 목록을 여러 곳에서 가리키면 한쪽의 변경이 다른 쪽에도 보이는 현상이 생깁니다. 이번 강좌의 반복과 포함 절에서 이 부분을 자세히 확인합니다.

이번 강좌에서는 목록의 생성, 이어 붙이기, 반복, 참조(인덱싱·슬라이싱), 중첩 목록을 다루며, 원소를 추가·삭제·정렬하는 함수는 제 9강에서 다룹니다.



생성

목록을 만드는 가장 기본적인 방법은 값을 쉼표(,)로 구분하여 대괄호([])로 감싸는 것입니다. 이렇게 소스 코드에 값을 직접 적어 만드는 방식을 리터럴(Literal) 표기라 합니다.

a = [1, 2, 3]
b = [4, 5, 6]

print(a)
print(b)
결과
[1, 2, 3]
[4, 5, 6]

목록은 대괄호([])와 쉼표(,)를 이용하여 생성이 가능합니다.

print()로 목록을 출력하면 대괄호와 쉼표를 포함하여 목록을 만들 때와 같은 모양으로 표시됩니다.


e = []
f = list(range(5))
g = [1, "Python", 3.14, True, None]

print(e)
print(f)
print(g)
결과
[]
[0, 1, 2, 3, 4]
[1, ‘Python’, 3.14, True, None]

[]는 빈 목록을 생성하며, list()를 이용하여 range() 등 다른 반복 가능한 객체를 목록으로 변환할 수 있습니다.

빈 목록은 처음에는 값이 없지만 나중에 값을 하나씩 채워 넣을 때 시작점으로 자주 사용합니다.

하나의 목록에 정수, 문자열, 실수, 논리값, None 등 서로 다른 자료형을 함께 저장할 수 있습니다. 목록은 값 자체가 아니라 값을 가리키는 참조를 저장하므로 원소마다 자료형이 달라도 문제가 없습니다.

출력 결과에서 문자열 "Python"이 'Python'처럼 작은따옴표로 표시되는 것은 목록을 출력할 때 원소를 코드에 쓰는 형태(repr)로 보여 주기 때문입니다.


print(list("Python"))
print(list((1, 2, 3)))

try:
    list(5)
except TypeError as err:
    print(err)
결과
[‘P’, ‘y’, ‘t’, ‘h’, ‘o’, ‘n’]
[1, 2, 3]
‘int’ object is not iterable

list(x)는 x에서 원소를 하나씩 꺼내어 새 목록을 만듭니다. 문자열은 문자를 하나씩 꺼낼 수 있으므로 list("Python")은 문자 6개짜리 목록이 됩니다.

튜플 (1, 2, 3)도 원소를 하나씩 꺼낼 수 있으므로 같은 원소를 가진 목록으로 변환됩니다.

반면 정수 5는 원소를 꺼낼 수 있는 객체가 아니므로 TypeError가 발생합니다. 숫자 하나를 담은 목록이 필요하다면 list(5)가 아니라 [5]로 작성해야 합니다.

  • Tip : list("Python")은 ["Python"]과 다릅니다. 전자는 문자 6개를 담은 목록이고, 후자는 문자열 하나를 담은 목록입니다.



이어 붙이기

두 목록을 하나로 합치거나 목록 뒤에 값을 덧붙여야 할 때는 + 연산자를 사용합니다. 숫자의 +가 덧셈인 것과 달리, 목록의 +는 두 목록의 원소를 순서대로 이어 붙인 목록을 만듭니다.

a = [1, 2, 3]
b = [4, 5, 6]
c = a + b
d = a + [9]

print(c)
print(d)
결과
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 9]

목록은 +를 이용하여 목록끼리 합치거나 새로운 값을 추가할 수 있습니다.

a + b는 a의 원소 1, 2, 3 뒤에 b의 원소 4, 5, 6을 차례대로 이어 붙인 [1, 2, 3, 4, 5, 6]이 되며, a + [9]는 a 뒤에 9 하나를 붙인 [1, 2, 3, 9]가 됩니다.

+는 새로운 목록을 생성하므로 a와 b의 값은 변경되지 않습니다. 두 목록의 원소를 모두 새 목록으로 옮겨 담기 때문에, 목록이 길수록 + 연산에 걸리는 시간도 길어집니다.


a = [1, 2, 3]
a += [4]
print(a)

try:
    [1, 2] + 3
except TypeError as err:
    print(err)
결과
[1, 2, 3, 4]
can only concatenate list (not “int”) to list

+=를 사용하면 기존 목록의 뒤에 값을 이어 붙입니다.

+는 목록끼리만 연산할 수 있으므로, 값을 추가할 때는 [9]처럼 대괄호로 감싸야 합니다. 정수를 그대로 더하면 TypeError가 발생합니다.

오류 메시지 can only concatenate list (not "int") to list는 “목록에는 목록만 이어 붙일 수 있는데 정수(int)가 들어왔다”는 뜻입니다.


a = [1, 2, 3]
b = a
a += [4]
print(a, b, a is b)

a = a + [5]
print(a, b, a is b)
결과
[1, 2, 3, 4] [1, 2, 3, 4] True
[1, 2, 3, 4, 5] [1, 2, 3, 4] False

a += [4]와 a = a + [4]는 결과가 같아 보이지만 동작 방식이 다릅니다.

b = a는 목록을 복사하지 않고 b도 같은 목록 객체를 가리키게 합니다. is 연산자는 두 변수가 같은 객체를 가리키는지 확인하며, 첫 줄에서 True가 출력됩니다.

+=는 새 목록을 만들지 않고 기존 목록을 직접 늘립니다. 그래서 a에 4를 붙이자 같은 객체를 가리키는 b에도 4가 보입니다.

반면 a = a + [5]는 +로 새 목록을 만든 뒤 그 목록을 a에 다시 연결합니다. 이제 a와 b는 서로 다른 객체이므로, b는 [1, 2, 3, 4]로 남고 a is b는 False가 됩니다.


a = [1, 2]
a += "xy"
print(a)

a += (3, 4)
print(a)
결과
[1, 2, ‘x’, ‘y’]
[1, 2, ‘x’, ‘y’, 3, 4]

+는 목록끼리만 연산할 수 있지만, +=는 문자열이나 튜플처럼 원소를 하나씩 꺼낼 수 있는 객체라면 무엇이든 받아 그 원소들을 뒤에 붙입니다.

그래서 a += "xy"는 "xy"라는 문자열 하나가 아니라 'x', 'y' 두 문자가 각각 추가됩니다. 문자열 하나를 통째로 추가하려면 a += ["xy"]처럼 대괄호로 감싸야 합니다.

  • Tip : +=는 9강에서 배우는 목록.extend()와 같은 방식으로 동작합니다.



반복

같은 값이 여러 번 들어간 목록이 필요할 때는 * 연산자로 목록을 반복합니다. 예를 들어 길이가 10이고 모든 값이 0인 목록은 [0] * 10으로 간단히 만들 수 있습니다.

a = [1, 2, 3]
b = [4, 5, 6]

print(a * 2)
print(b * 3)
결과
[1, 2, 3, 1, 2, 3]
[4, 5, 6, 4, 5, 6, 4, 5, 6]

목록은 *를 이용하여 목록을 반복시켜 목록 자체의 크기가 커집니다.

a * 2는 [1, 2, 3]을 두 번 이어 붙인 길이 6의 목록이고, b * 3은 세 번 이어 붙인 길이 9의 목록입니다. +와 마찬가지로 새로운 목록을 반환하며 원본 a, b는 변하지 않습니다.


print([1, 2] * 0)
print([1, 2] * -1)
print(3 * [0])
결과
[]
[]
[0, 0, 0]

0이나 음수를 곱하면 오류 대신 빈 목록이 반환됩니다.

3 * [0]처럼 정수를 앞에 써도 같은 결과가 나옵니다. 단, 곱할 수 있는 값은 정수뿐이며 [0] * 2.5처럼 실수를 곱하면 TypeError가 발생합니다.


grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)

grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid)
결과
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
[[1, 0, 0], [0, 0, 0], [0, 0, 0]]

*는 목록 안의 원소를 복사하지 않고 같은 객체를 반복해서 참조합니다.

그래서 [[0] * 3] * 3으로 만든 2차원 목록은 세 개의 행이 모두 같은 목록이므로, 하나의 값을 변경하면 모든 행이 함께 변경됩니다.

첫 번째 결과를 보면 grid[0][0]만 바꿨는데 세 행의 첫 칸이 모두 1이 되었습니다. 겉보기에는 행이 세 개지만 실제로는 하나의 행 목록을 세 번 가리키고 있기 때문입니다.

[0] * 3처럼 정수나 문자열 같은 변경할 수 없는 값을 반복할 때는 이 문제가 드러나지 않습니다. 값을 “바꾸는” 것은 그 칸이 다른 객체를 가리키게 하는 것이기 때문입니다. 문제는 목록처럼 변경 가능한 객체를 *로 반복할 때 생깁니다.

2차원 목록을 만들 때는 [[0] * 3 for _ in range(3)]처럼 목록 내포(List Comprehension)를 사용하여 행마다 새로운 목록을 생성합니다. 두 번째 결과처럼 첫 번째 행만 변경되는 것을 확인할 수 있습니다.

  • Tip : for를 사용한 목록 내포는 반복문 강좌에서 자세히 다룹니다.



참조

목록에 저장된 값을 꺼내는 방법은 두 가지입니다. 원소 하나를 꺼내는 인덱싱(Indexing)과, 원소 여러 개를 구간으로 잘라 내는 슬라이싱(Slicing)입니다.

a = [1, 2, 3]
b = [4, 5, 6]
c = [1, 2, 3, 4, 5, 6, 7, 8, 9]

print(a[0])
print(a[1])
print(a[2])
print(a[-1])
print(b[0:1])
print(b[0:-1])
print(c[0:-1:2])
결과
1
2
3
3
[4]
[4, 5]
[1, 3, 5, 7]

목록은 대괄호([])와 index를 이용하여 목록을 참조할 수 있으며, :을 이용하여 목록의 일부를 잘라낼(Slicing) 수 있습니다.

목록[index]를 사용하면 index의 값을 출력합니다.

목록[start:end]를 사용하면 start부터 end 직전까지 출력합니다. end 위치의 값은 포함되지 않습니다.

목록[start:end:step]를 사용하면 start부터 end 직전까지 step 간격만큼 출력합니다.

좌측의 첫 번째 값은 0이며, 우측의 첫 번째 값은 -1을 사용하여 출력이 가능합니다.

그래서 b[0:-1]은 마지막 값 6을 제외한 [4, 5]가 되며, c[0:-1:2]는 마지막 값 9를 제외하고 두 칸씩 건너뛴 [1, 3, 5, 7]이 됩니다.


목록 b = [4, 5, 6]의 위치 번호를 표로 나타내면 다음과 같습니다.

원소 4 5 6
양수 index 0 1 2
음수 index -3 -2 -1

음수 index는 len(목록) + index로 바뀌어 계산됩니다. 길이가 3인 목록에서 -1은 3 + (-1) = 2번 위치, 즉 마지막 원소입니다.

인덱싱과 슬라이싱의 결과 형태가 다르다는 점도 중요합니다. b[0]은 원소 4 자체를 반환하지만, b[0:1]은 원소가 하나뿐이어도 [4]처럼 목록을 반환합니다.

  • Tip : 슬라이싱의 start:end를 “칸과 칸 사이의 경계”로 생각하면 이해하기 쉽습니다. 0:2는 0번 경계부터 2번 경계까지이므로 0번과 1번 원소, 즉 end - start = 2개의 원소를 잘라 냅니다.


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

print(c[:3])
print(c[6:])
print(c[:])
print(c[::2])
print(c[::-1])
print(c[-3:])
print(c[5:100])
결과
[1, 2, 3]
[7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 3, 5, 7, 9]
[9, 8, 7, 6, 5, 4, 3, 2, 1]
[7, 8, 9]
[6, 7, 8, 9]

start를 생략하면 처음부터, end를 생략하면 끝까지 잘라냅니다. c[:]는 목록 전체를 복사한 새로운 목록을 반환합니다.

c[:3]은 앞에서 세 개, c[6:]는 6번 위치부터 끝까지, c[-3:]는 뒤에서 세 개를 잘라 냅니다. “앞에서 n개”는 [:n], “뒤에서 n개”는 [-n:]으로 기억하면 편리합니다.

step에 음수를 사용하면 역방향으로 잘라내므로, c[::-1]은 뒤집힌 목록을 반환합니다.

c[5:100]처럼 잘라내는 범위가 목록의 길이를 넘어가도 오류 없이 존재하는 값까지만 반환합니다.


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

try:
    c[100]
except IndexError as err:
    print(err)
결과
list index out of range

반면 목록[index]로 존재하지 않는 위치를 참조하면 IndexError가 발생합니다.

슬라이싱은 “해당 구간에 있는 원소들”을 돌려주므로 구간이 비어 있으면 빈 목록을 반환하면 되지만, 인덱싱은 반드시 원소 하나를 돌려주어야 하므로 그 위치에 원소가 없으면 오류를 낼 수밖에 없습니다.

길이가 n인 목록에서 사용할 수 있는 index는 0부터 n-1까지, 음수로는 -n부터 -1까지입니다. 특히 마지막 원소를 꺼내려고 목록[len(목록)]을 사용하는 실수가 잦으므로, 마지막 원소는 목록[-1]로 참조합니다.


a = [1, 2, 3]
b = a
c = a[:]

print(b is a, c is a, c == a)

a[0] = 100
print(b)
print(c)
결과
True False True
[100, 2, 3]
[1, 2, 3]

슬라이싱은 항상 새로운 목록을 만듭니다. 그래서 a[:]는 목록을 복사하는 가장 간단한 방법입니다.

b = a는 같은 객체를 가리키므로 b is a가 True이고, c = a[:]는 내용은 같지만 다른 객체이므로 c is a는 False, c == a는 True입니다.

==는 내용(값)이 같은지, is는 같은 객체인지를 비교합니다. 이후 a[0]을 변경하면 같은 객체인 b에는 변경이 보이고, 복사본인 c는 원래 값을 유지합니다.



포함

목록의 원소로 다시 목록을 넣으면 표(행과 열)처럼 2차원 구조의 데이터를 표현할 수 있습니다. 이를 중첩 목록(Nested List)이라 합니다.

a = [1, 2, 3]
b = [4, 5, 6]
c = [a, b]

print(c)
결과
[[1, 2, 3], [4, 5, 6]]

목록안에 목록을 포함시켜 목록을 포함한 목록 또한 생성이 가능합니다.

c는 원소가 a와 b 두 개인 목록입니다. 겉에 있는 목록의 길이는 안쪽 원소의 개수와 상관없이 len(c) == 2입니다.


a = [1, 2, 3]
b = [4, 5, 6]
c = [a, b]

print(c[1][0])

a[0] = 100
print(c)
결과
4
[[100, 2, 3], [4, 5, 6]]

c[1][0]처럼 대괄호를 이어서 사용하면 안쪽 목록의 값을 참조할 수 있습니다. c[1]은 b이며, b[0]은 4입니다.

대괄호는 왼쪽부터 차례대로 계산됩니다. c[1]이 먼저 계산되어 목록 [4, 5, 6]이 되고, 그 결과에 [0]을 적용하여 4를 얻습니다. 2차원 목록에서는 목록[행][열] 순서로 읽으면 됩니다.

목록 c는 a와 b의 값을 복사하지 않고 같은 목록 객체를 참조합니다. 그래서 a의 값을 변경하면 c의 내용도 함께 변경됩니다.


import copy

a = [[1, 2], [3, 4]]
b = a[:]
c = copy.deepcopy(a)

a[0][0] = 100

print(b)
print(c)
결과
[[100, 2], [3, 4]]
[[1, 2], [3, 4]]

중첩 목록을 복사할 때는 주의가 필요합니다. a[:]는 바깥 목록만 새로 만들고, 안쪽 목록은 원본과 같은 객체를 그대로 가리킵니다. 이를 얕은 복사(Shallow Copy)라 합니다.

그래서 a[0][0]을 변경하면 얕은 복사본 b에서도 [[100, 2], [3, 4]]로 변경된 값이 보입니다.

안쪽 목록까지 모두 새로 복사하려면 copy 모듈의 copy.deepcopy()를 사용합니다. 이를 깊은 복사(Deep Copy)라 하며, c는 원본이 바뀌어도 [[1, 2], [3, 4]]를 유지합니다.

  • Tip : 안쪽에 목록이 없는 1차원 목록이라면 얕은 복사만으로 충분합니다. 원소가 정수·문자열처럼 변경할 수 없는 값이기 때문입니다.



정리

이름 설명 반환값/특징
[a, b, c] 리터럴로 목록 생성 새 목록
list(x) 반복 가능한 객체의 원소로 목록 생성 새 목록, 정수 등은 TypeError
목록1 + 목록2 두 목록을 이어 붙임 새 목록, 원본 유지
목록 += x 원본 뒤에 x의 원소를 붙임 원본 변경, 반복 가능한 객체 허용
목록 * n 목록을 n번 반복 새 목록, 원소는 복사하지 않고 참조
목록[i] i번 위치의 원소 원소 자체, 범위 밖이면 IndexError
목록[start:end:step] 구간을 잘라 냄 새 목록(얕은 복사), 범위 밖이어도 오류 없음
목록[i][j] 중첩 목록의 안쪽 원소 왼쪽 대괄호부터 차례로 계산
copy.deepcopy(x) 안쪽 객체까지 모두 복사 새 객체

목록은 변경 가능하고, 순서가 있으며, 중복을 허용하는 자료형입니다. index로 원소 하나를 꺼내는 것은 위치만 계산하면 되므로 목록의 길이와 상관없이 빠릅니다.

+, *, 슬라이싱은 새 목록을 만들고, +=는 원본을 변경합니다. 목록은 값이 아니라 참조를 담는다는 점을 기억하면 * 반복과 중첩 목록 복사에서 생기는 의외의 결과를 이해할 수 있습니다.

댓글 남기기