
2차원 리스트는 가로(열) × 세로(행) 형태로 이루어진 리스트 안의 리스트입니다. 행(row)과 열(column) 모두 0부터 시작합니다.
# 3행 4열의 2차원 리스트
matrix = [
[1, 2, 3, 4], # 행 0
[5, 6, 7, 8], # 행 1
[9, 10, 11, 12] # 행 2
]
시각적으로 표현하면 다음과 같습니다.
| 열 0 | 열 1 | 열 2 | 열 3 | |
|---|---|---|---|---|
| 행 0 | 1 | 2 | 3 | 4 |
| 행 1 | 5 | 6 | 7 | 8 |
| 행 2 | 9 | 10 | 11 | 12 |
2차원 리스트의 요소에 접근하거나 값을 할당할 때는 대괄호 []를 두 번 사용합니다. 첫 번째가 행, 두 번째가 열입니다.
# 요소 접근: 리스트[행][열]
print(matrix[1][2]) # 7 (행 1, 열 2)
# 값 할당
matrix[0][3] = 99
print(matrix[0]) # [1, 2, 3, 99]
파이썬에서는 각 행의 길이가 다른 톱니형 리스트도 만들 수 있습니다. 다른 언어에서는 제약이 있는 경우가 많지만, 파이썬은 리스트 안에 서로 다른 길이의 리스트를 자유롭게 넣을 수 있습니다.
# 각 행의 길이가 다른 톱니형 리스트
jagged = [
[1, 2],
[3, 4, 5, 6],
[7, 8, 9]
]
print(jagged[1][3]) # 6
2차원 튜플도 존재합니다. 다만 튜플은 내용을 변경할 수 없으므로, 조회만 가능하다는 점에 유의해야 합니다.
# 2차원 튜플
matrix_tuple = (
(1, 2, 3),
(4, 5, 6),
(7, 8, 9)
)
print(matrix_tuple[2][1]) # 8
# 값 변경 시도 → 에러 발생
# matrix_tuple[0][0] = 99 # TypeError!
가장 간결한 방법입니다. 바깥 for에서 행을 꺼내고, 안쪽 for에서 각 요소를 꺼냅니다.
# for-in 중첩으로 모든 요소 출력
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
for row in matrix:
for element in row:
print(element, end=' ')
print() # 행이 끝나면 줄바꿈
# 1 2 3
# 4 5 6
# 7 8 9
인덱스가 필요한 경우 range를 활용합니다.
# for-range로 인덱스를 사용한 접근
for i in range(len(matrix)):
for j in range(len(matrix[i])):
print(f"[{i}][{j}] = {matrix[i][j]}", end=' ')
print()
while로도 동일하게 순회할 수 있습니다. 인덱스를 직접 관리해야 하므로 코드가 다소 길어집니다.
# while 반복문으로 2차원 리스트 순회
i = 0
while i < len(matrix):
j = 0
while j < len(matrix[i]):
print(matrix[i][j], end=' ')
j += 1
print()
i += 1
💡 일반적으로는 for-in 방식이 가장 파이썬답고 간결합니다. 인덱스가 꼭 필요한 경우에만
range나enumerate를 사용하세요.
리스트 컴프리헨션을 중첩하면 2차원 리스트를 간결하게 만들 수 있습니다.
# 3행 4열 2차원 리스트 생성 (모든 요소 0으로 초기화)
matrix = [[0 for col in range(4)] for row in range(3)]
print(matrix)
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
# 톱니형 리스트: 각 행의 길이가 행 번호 + 1
jagged = [[0 for col in range(row + 1)] for row in range(3)]
print(jagged)
# [[0], [0, 0], [0, 0, 0]]
* 연산자로 만들면 안 되는 이유2차원 리스트를 만들 때 흔히 하는 실수가 있습니다.
# ❌ 잘못된 방법: 같은 리스트를 참조하게 됨
wrong = [[0] * 4] * 3
wrong[0][0] = 99
print(wrong)
# [[99, 0, 0, 0], [99, 0, 0, 0], [99, 0, 0, 0]] ← 모든 행이 변경됨!
# ✅ 올바른 방법: 리스트 컴프리헨션 사용
correct = [[0] * 4 for _ in range(3)]
correct[0][0] = 99
print(correct)
# [[99, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] ← 행 0만 변경됨
* 연산자로 행을 복제하면 모든 행이 같은 리스트 객체를 참조하게 되어, 하나를 바꾸면 전부 바뀝니다. 이전 포스트에서 다룬 할당 vs 복사 문제와 동일한 원리입니다.
이전 포스트에서 copy() 메서드를 다뤘는데, 이는 얕은 복사(shallow copy)였습니다. 2차원 리스트처럼 중첩된 구조에서는 copy()로는 부족합니다.
deepcopy는 중첩된 리스트(튜플) 안에 있는 모든 리스트(튜플)까지 재귀적으로 복사해줍니다.
import copy
# 얕은 복사: 내부 리스트는 여전히 공유
a = [[1, 2], [3, 4]]
b = a.copy()
b[0][0] = 999
print(a) # [[999, 2], [3, 4]] ← a도 변경됨!
# 깊은 복사: 완전히 독립된 리스트
a = [[1, 2], [3, 4]]
c = copy.deepcopy(a)
c[0][0] = 999
print(a) # [[1, 2], [3, 4]] ← a는 변경되지 않음
얕은 복사와 깊은 복사의 차이를 정리하면 다음과 같습니다.
| 구분 | copy() / list() | copy.deepcopy() |
|---|---|---|
| 복사 깊이 | 최상위 리스트만 복사 | 모든 중첩 리스트까지 복사 |
| 내부 리스트 | 원본과 공유 | 완전히 독립 |
| 사용 시점 | 1차원 리스트 | 2차원 이상 중첩 리스트 |