리스트(list, 목록)는 파이썬에서 가장 자주 쓰는 그릇입니다. 값을 순서대로 담고, 각 자리에 0부터 번호를 매겨 두기 때문에 원하는 자리를 바로 집어낼 수 있습니다. 이 번호를 인덱스(index, 순번)라고 합니다. 리스트를 자유롭게 다루는 것이 자료구조 공부의 기본기이므로, 값을 꺼내고 잘라 내고 하나씩 훑는 방법을 예제로 차근차근 익히겠습니다. 코드는 짧으니 꼭 직접 쳐 보시기 바랍니다. 리스트는 쓰임이 워낙 많아, 여기서 다지는 기본기가 이후 거의 모든 예제에서 되풀이됩니다.

[자료구조 03] 리스트, 순서대로 담는 그릇

03.1 인덱스로 값 꺼내기

과일 목록에서 첫 번째와 마지막 과일을 꺼내 보겠습니다.


fruits = ["사과", "바나나", "포도"]

print(fruits[0])

print(fruits[-1])


사과

포도


인덱스는 0부터 시작합니다. 그래서 fruits[0]이 첫 번째 값입니다. 두 번째는 [1], 세 번째는 [2]가 됩니다. 음수를 쓰면 뒤에서부터 세는데, -1이 맨 마지막, -2가 끝에서 두 번째입니다. 맨 뒤 값을 꺼낼 때 길이를 몰라도 [-1] 하나면 되니 아주 편리합니다. 리스트가 아무리 길어도 이렇게 자리를 집어 꺼내는 일은 즉시 끝나므로 O(1)입니다. 다만 인덱스를 리스트 길이보다 크게 주면 IndexError라는 오류가 납니다. 원소가 세 개인데 [5]를 꺼내려 하면 없는 자리라 오류가 나니, 인덱스 범위에 주의해야 합니다. 자주 쓰는 [0]과 [-1]만 확실히 익혀 두어도 리스트 다루기가 한결 편해집니다.

03.2 슬라이스로 일부만 잘라 내기

목록에서 앞의 두 개만 뽑아 보겠습니다.


fruits = ["사과", "바나나", "포도"]

print(fruits[0:2])


['사과', '바나나']


[시작:끝] 형태를 슬라이스(slice, 잘라 내기)라고 합니다. 초보가 가장 많이 헷갈리는 지점이 여기입니다. 슬라이스는 끝 번호 직전까지 자르고 끝 번호 자체는 포함하지 않습니다. 그래서 [0:2]는 0번과 1번만 담고 2번은 빼놓습니다. 담기는 개수는 끝에서 시작을 뺀 만큼, 여기서는 2에서 0을 뺀 두 개입니다. 슬라이스는 잘라 낸 값들로 새 리스트를 만들므로, 뽑는 개수 k에 비례해 O(k)입니다. 시작을 비워 [:2]라고 쓰면 처음부터, 끝을 비워 [1:]이라고 쓰면 끝까지라는 뜻이 됩니다. 양쪽을 다 비운 [:]는 리스트 전체를 새로 복사한 것과 같습니다. 이 규칙 때문에 [1:3]은 1번과 2번, 두 칸만 가져온다는 점을 늘 떠올리면 실수가 줄어듭니다.

03.3 번호와 값을 함께 훑기

목록을 처음부터 끝까지 하나씩 도는 것을 순회라고 합니다. 각 과일 앞에 순번을 붙여 출력해 보겠습니다.


fruits = ["사과", "바나나", "포도"]

out = []

for i, f in enumerate(fruits):

out.append(f"{i}:{f}")

print(out)


['0:사과', '1:바나나', '2:포도']


enumerate는 순회하면서 (순번, 값)을 함께 건네줍니다. 그래서 for i, f처럼 변수를 두 개 받아 번호와 값을 동시에 씁니다. 순번이 필요할 때 range(len(...))로 번거롭게 돌리는 것보다 훨씬 깔끔합니다. 이 코드는 원소를 한 번씩 모두 훑으므로 O(n)입니다. 순번이 필요 없이 값만 쓰면 될 때는 for f in fruits처럼 값 하나만 받아 돌리면 더 간단합니다. 리스트뿐 아니라 문자열이나 튜플처럼 순서가 있는 자료도 같은 방식으로 순회할 수 있습니다. 순회는 리스트에서 가장 자주 하는 일이니 손에 잘 익혀 두시면 좋습니다.

03.4 리스트 안의 리스트, 표 만들기

리스트 안에 또 리스트를 넣으면 표나 격자를 표현할 수 있습니다.


grid = [[1, 2, 3],

[4, 5, 6]]

print(grid[1][2])


6


바깥 리스트의 각 원소가 한 줄(행)이 됩니다. grid[1]은 두 번째 행인 [4, 5, 6]이고, 거기서 다시 [2]로 세 번째 값 6을 꺼냅니다. 그래서 [행][열] 순서로 접근한다고 기억하면 됩니다. 이런 2차원 리스트는 나중에 표 데이터나 격자 지도, 그래프의 연결 관계를 담을 때 두루 쓰입니다. 행과 열의 순서를 바꿔 grid[2][1]처럼 잘못 쓰면 없는 자리라 오류가 날 수 있으므로, 항상 바깥이 행이고 안이 열이라는 순서를 지켜야 합니다.

03.5 값 바꾸기와 개수 세기

두 번째 과일을 딸기로 바꾸고, 전체 개수를 세어 보겠습니다.


fruits = ["사과", "바나나", "포도"]

fruits[1] = "딸기"

print(fruits, len(fruits))


['사과', '딸기', '포도'] 3


리스트는 담은 뒤에도 자리의 값을 자유롭게 바꿀 수 있습니다. fruits[1]에 새 값을 대입하니 바나나가 딸기로 교체되었습니다. 이렇게 바꿀 수 있는 성질을 가변(mutable)이라고 합니다. len()은 원소 개수를 돌려주는데, 파이썬은 리스트 길이를 따로 기억해 두므로 개수가 아무리 많아도 O(1)에 알려 줍니다. 반대로 문자열은 값을 바꿀 수 없는 불변(immutable, 바꿀 수 없는) 자료라, 리스트와 헷갈리지 않도록 구분해 두면 좋습니다.

03.6 초보가 자주 걸리는 함정, 대입은 복사가 아닙니다

리스트를 다른 이름에 넘길 때 조심할 점이 하나 있습니다. 언뜻 복사한 것처럼 보이지만 그렇지 않습니다.


x = [1, 2, 3]

y = x # 복사가 아닙니다

y.append(4)

print(x)


[1, 2, 3, 4]


y만 건드렸는데 x까지 바뀌었습니다. y = x는 새 리스트를 만드는 게 아니라, 같은 리스트에 이름표를 하나 더 붙이는 것이기 때문입니다. 두 이름이 같은 그릇을 가리키니, 한쪽으로 바꾸면 다른 쪽에도 그대로 보입니다.


진짜 복사가 필요하면 x[:]나 x.copy()로 내용을 떠 와야 합니다. 다만 리스트 안에 다시 리스트가 들어 있는 경우에는 이 방법으로도 안쪽까지 복사되지 않으니, 그럴 땐 copy 모듈의 deepcopy를 씁니다.


import copy

o2 = [[1, 2], [3, 4]]

deep = copy.deepcopy(o2)

deep[0][0] = 99

print(o2)


[[1, 2], [3, 4]]


deepcopy로 떠 왔기 때문에 사본을 바꿔도 원본 o2는 그대로입니다. 이 차이를 모르면 원본이 몰래 바뀌는 버그로 한참 헤맬 수 있으니, 대입과 복사는 다르다는 점을 꼭 기억해 두시기 바랍니다. 특히 리스트를 함수에 넘겨 그 안에서 값을 바꾸면 바깥의 원본까지 바뀌는데, 이것도 같은 원리 때문입니다.

03.7 리스트 기본기 정리

리스트는 인덱스로 O(1)에 값을 집고, 슬라이스로 일부를 잘라 내며, enumerate로 번호와 값을 함께 훑습니다. 안에 리스트를 넣어 표도 만들고, 자리의 값도 자유롭게 바꿉니다. 다만 대입(=)은 복사가 아니라는 점, 슬라이스는 끝 번호를 포함하지 않는다는 점은 초보가 꼭 짚고 넘어가야 할 대목입니다. 이 기본 동작들을 손에 익히는 것이 리스트를 다루는 첫걸음입니다. 직접 값을 바꿔 가며 출력해 보면 인덱스와 슬라이스의 경계가 훨씬 또렷하게 잡힙니다. 리스트는 앞으로 배울 스택, 큐 같은 다른 자료구조의 바탕이 되기도 하니, 여기서 확실히 다져 두면 뒤가 한결 수월합니다. 작은 예제라도 손으로 직접 쳐 보는 것이 눈으로 읽기만 하는 것보다 몇 배는 오래 남습니다.