6. 리스트 (List)

파이썬에서 가장 대중적이고 활용도가 높은 자료형이 바로 리스트(List)입니다.

리스트는 다른 언어의 배열(Array)과 구조가 유사하지만, 안에 담기는 원소들의 자료형이 서로 달라도 문제가 없으며, 생성된 이후에도 자유롭게 원소를 갱신, 추가, 삭제할 수 있는 변경 가능(Mutable)한 특성을 가진 강력한 컬렉션입니다.

6.1 리스트의 생성과 기본 조작

대괄호 [] 를 이용해 생성하거나 list() 생성자를 통해 다른 시퀀스(튜플, 문자열 등)를 리스트로 변환할 수 있습니다.

empty_list = []
num_list = [1, 2, 3]

# 문자열을 리스트로 쪼개기
char_list = list("가나다") 
print(char_list) # ['가', '나', '다']

리스트가 제공하는 대표적인 내장 조작 메서드는 다음과 같습니다.

  • append(값): 리스트의 제일 마지막 꼬리에 원소를 하나 추가합니다.
  • insert(위치, 값): 원하는 인덱스 번호 중간에 원소를 끼워 넣습니다.
  • pop(위치): 지정한 인덱스의 원소를 리스트에서 완전히 뽑아냅니다. (값을 반환함)
  • remove(값): 리스트에서 해당 값을 찾아 삭제합니다. (중복 시 맨 앞 요소 1개만 지움)
  • sort() / reverse(): 리스트 내부의 순서를 오름차순으로 정렬하거나, 반대로 뒤집습니다.

6.2 변경 가능(Mutable) 객체의 함정: 별칭(Alias)

리스트를 다룰 때 초보자가 가장 흔하게 겪는 버그는 메모리 참조(Reference)에 관련된 문제입니다. 파이썬에서 변수에 객체를 할당하는 행위는 값을 복사해 넣는 것이 아니라 ‘객체에 이름표를 붙이는 행위’이기 때문입니다.

original = [1, 2, 3]

# 할당 연산자(=)는 값을 복사하지 않습니다! 
# 단지 original과 "동일한 메모리"를 가리키는 별칭(Alias)을 하나 더 만들 뿐입니다.
alias_list = original 

# 별칭 리스트의 원소를 바꿨는데...
alias_list[0] = 999 

# 원본 리스트도 같이 바뀌어버립니다!
print(original) # [999, 2, 3]

단순히 A = B 형태로 리스트를 대입하면 두 변수는 완벽히 운명 공동체가 됩니다. 이를 방지하고 싶다면 반드시 복사(Copy)를 수행해야 합니다.


6.3 리스트 복사 (copy()) 와 얕은 복사의 한계

원본과 별개의 독립적인 리스트를 만들고 싶을 때는 copy() 메서드를 사용합니다.

original = [1, 2, 3]
copied = original.copy()

copied[0] = 999
print(original) # [1, 2, 3] (원본은 타격받지 않음)

얕은 복사(Shallow Copy)의 한계

하지만 리스트 내부에 또 다른 리스트가 들어있는 중첩 리스트(2차원 배열 등) 상황에서는 copy() 만으로 완벽한 분리가 불가능합니다. 껍데기 리스트만 새로 만들어질 뿐, 그 안에 담긴 내부 리스트의 주소는 여전히 공유(얕은 복사)되기 때문입니다.

# 내부에 또 다른 리스트를 품은 2차원 리스트
matrix = [[1, 2], [3, 4]]

# 껍데기를 복사
shallow = matrix.copy()

# 내부 리스트의 값을 건드리는 순간?
shallow[0][0] = 999

# 원본 내부까지 같이 오염됩니다. (얕은 복사의 한계)
print(matrix) # [[999, 2], [3, 4]]

이러한 다차원 구조에서 내부 객체들의 뼈대까지 샅샅이 뒤져 “완벽히 독립된 도플갱어”를 만들어야 할 때는 파이썬 표준 라이브러리인 copy 모듈의 deepcopy() (깊은 복사) 기능을 사용해야만 합니다.


6.4 리스트 곱셈 연산 시의 초기화 버그

다차원 리스트를 만들기 위해 정수 곱셈(*)을 사용할 때도 위와 동일한 참조(Reference) 얕은 복사 문제가 발생합니다.

# [3, 3, 3] 을 만들고, 그걸 3번 반복해서 2차원 리스트를 만듦
matrix = [[3] * 3] * 3
print(matrix) # [[3, 3, 3], [3, 3, 3], [3, 3, 3]]

# 첫 번째 행, 첫 번째 열을 99로 바꿨는데...
matrix[0][0] = 99

# 모든 행의 첫 번째 열이 99로 바뀝니다!
print(matrix) # [[99, 3, 3], [99, 3, 3], [99, 3, 3]]

* 3 으로 늘린 3개의 내부 리스트는 모두 똑같은 메모리 주소를 가리키는 거울 속 쌍둥이일 뿐이기 때문입니다. 이런 일을 막으려면 지능형 리스트(리스트 컴프리헨션)를 이용해 순환할 때마다 [3] * 3을 새로 생성하도록 짜야 합니다.

# "안전한" 2차원 리스트 초기화 방법 (리스트 컴프리헨션)
matrix_safe = [[3] * 3 for _ in range(3)]

matrix_safe[0][0] = 99
print(matrix_safe) # [[99, 3, 3], [3, 3, 3], [3, 3, 3]] (오직 첫 요소만 정상 변경)
서브목차