Skip to content
This page has been auto-translated and may contain errors.View in English

리스트

변수는 한 가지만 담습니다. 리스트는 여러 가지를 순서대로, 하나의 이름 아래 담습니다. 리더보드는 순위가 매겨진 점수들의 나열이고, 퀴즈는 문제들의 모음입니다. 관련된 값들의 묶음을 관리해야 하는 순간부터 리스트가 필요해집니다.

리스트는 파이썬에서 범용으로 쓰이는 순서가 있고 변경 가능한(mutable) 시퀀스입니다. 시간이 지나면서 항목이 추가되거나 제거되고, 순서가 뒤바뀌고, 내용이 걸러지거나 정렬되는 등 계속 변하는 대상을 다루기에 딱 맞습니다. 순서가 중요하고 컬렉션이 계속 바뀐다면, 대개 리스트가 첫 번째로 고려할 선택입니다.

list는 파이썬의 동적 배열로, 순서가 있고 변경 가능한(mutable, 내부 값을 직접 바꿀 수 있는) 시퀀스입니다. 성능 특성이 설계 판단을 좌우합니다. 인덱스로 읽는 연산은 O(1), 즉 리스트 크기와 무관하게 상수 시간이 걸립니다(big-O는 크기 n에 따라 비용이 어떻게 늘어나는지를 나타내는 표기법입니다). append()도 실질적으로 O(1)인데, 리스트가 끝부분에 여유 공간을 남겨두고 가끔씩만 크기를 다시 조정하기 때문입니다. 문제가 되는 연산은 O(n)인 insert()remove()로, 변경 지점 이후의 모든 요소가 한 칸씩 이동해야 하기 때문입니다. 반복문에서 맨 앞에 삽입하거나 제거하는 상황을 발견했다면, 그 O(n)이 다른 구조를 써야 한다는 신호입니다.

리스트 만들기

대괄호 안에 값을 쉼표로 구분해 나열합니다. 리스트는 어떤 타입이든 섞어서 담을 수 있고, 빈 리스트도 유효하며 시간을 두고 채워나가는 시작점으로 흔히 쓰입니다.

리스트는 대괄호 문법으로 정의하며 삽입한 순서를 그대로 유지합니다. 다른 리스트를 포함해 어떤 파이썬 값이든 담을 수 있습니다. 빈 리스트 []는 항목을 하나씩 누적해 나갈 때 표준적으로 쓰는 시작점입니다.

대괄호 리터럴은 새로운 list 객체를 만들며, 리스트는 값 자체를 복사해 담는 것이 아니라 각 원소에 대한 참조를 저장합니다(참조는 실제 객체가 있는 위치를 가리키는 포인터입니다). 따라서 원소는 다른 리스트를 포함해 어떤 파이썬 객체든 될 수 있습니다. 한 리스트에 여러 타입을 섞는 것은 문법적으로는 가능하지만 간단한 스크립트를 제외하면 흔치 않습니다. 실제로 유지하는 리스트는 대부분 한 가지 타입만 담는데, 그래야 추론하기도 쉽고 반복문에서 처리하기도 쉽기 때문입니다.

python
scores = [87, 92, 74, 65, 91]
players = ["민준", "서연", "지호"]
mixed = ["민준", 87, True, 3.14]   # 어떤 타입이든 되지만 흔치 않음
empty = []
Juno리스트 만들기 대괄호 사이에 값을 쉼표로 구분해 넣으면, 파이썬은 여러분이 쓴 순서 그대로 보관합니다. 앞으로 채워나갈 계획이라면 빈 []로 시작해도 좋습니다. 제 리스트들도 대부분 그렇게, 비어 있는 채로 기다리며 시작합니다.
Juno리스트 만들기 대괄호로 리스트를 정의하고, 삽입 순서가 그대로 유지됩니다. 리스트는 다른 리스트를 포함해 어떤 값이든 담을 수 있고, 항목을 하나씩 쌓아나갈 때는 []가 표준적인 시작점입니다.
Juno리스트 만들기 리스트는 원소에 대한 참조를 담기 때문에 어떤 객체든 섞을 수 있지만, 실제로 유지하고 싶은 것은 한 리스트에 한 가지 타입입니다. []로 시작해 쌓아나가세요. 여유 공간을 미리 확보해두기 때문에 비용도 적게 듭니다.

인덱싱과 슬라이싱

리스트는 문자열과 같은 번호 체계를 씁니다. 위치는 0부터 시작하고, 음수는 끝에서부터 셉니다. 위치를 지정해 어떤 항목이든 읽을 수 있습니다. 리스트는 변경 가능하므로 특정 위치에 값을 쓸 수도 있습니다.

리스트의 인덱싱과 슬라이싱은 문자열과 같은 규칙을 따릅니다. 핵심적인 차이는 가변성입니다. 인덱스나 슬라이스에 값을 대입해 항목을 직접 바꿀 수 있는데, 문자열은 이를 허용하지 않습니다.

인덱싱과 슬라이싱은 문자열과 읽는 방식이 같지만, 리스트는 변경 가능하므로 값을 대입할 수도 있습니다. 알아둘 것은 슬라이스 대입인데, 이는 리스트의 길이를 바꿀 수 있습니다. lst[1:3] = [10, 20, 30]은 1번과 2번 위치의 두 항목을 세 개의 새 항목으로 바꿔서, 리스트가 하나 더 늘어납니다. 대입하는 값의 개수가 슬라이스의 길이와 꼭 맞지 않아도 되는데, 이는 편리하면서도 흔히 놀라게 되는 부분이니 의도적으로 활용하고 우연히 쓰지는 않도록 하세요.

python
scores = [87, 92, 74, 65, 91]

scores[0]      # 87  (첫 번째)
scores[-1]     # 91  (마지막)
scores[1:3]    # [92, 74]
scores[:2]     # [87, 92]
scores[::-1]   # [91, 65, 74, 92, 87]  (뒤집힌 순서)

scores[0] = 90   # 가변: 동작함 (문자열은 TypeError 발생)
Juno인덱싱과 슬라이싱 위치는 0부터 시작하고, 음수는 끝에서부터 거꾸로 세므로 scores[-1]이 마지막 항목입니다. scores[1:3] 같은 슬라이스는 1번과 2번 위치를 담은 새 리스트를 돌려줍니다. 잠깐 걸렸던 부분: 리스트는 위치에 값을 쓸 수도 있다는 것, scores[0] = 90처럼요. 문자열이라면 거부당했을 겁니다.
Juno인덱싱과 슬라이싱 문자열과 같은 인덱싱, 슬라이싱 규칙입니다. 위치는 0부터, 음수는 끝에서부터입니다. 차이는 가변성인데, 인덱스나 슬라이스에 대입하면 리스트가 그 자리에서 바뀌지만, 문자열이라면 TypeError가 발생합니다.
Juno인덱싱과 슬라이싱 문자열 인덱싱처럼 읽히지만 대입도 가능하고, 슬라이스 대입은 리스트 크기를 바꿀 수도 있습니다. lst[1:3] = [10, 20, 30]은 두 항목을 세 항목으로 바꿔치기하는데, 의도적으로 쓰면 유용하고 우연히 마주치면 놀라니 신경 써서 사용하세요.

항목 추가하기

항목을 추가하는 방법은 세 가지입니다. append()는 끝에 항목 하나를 추가하며, 거의 항상 쓰게 될 방법입니다. insert()는 특정 위치에 추가합니다. extend()는 다른 리스트를 합쳐 넣습니다.

append()는 상각(amortised) O(1)입니다(리스트가 여유 공간을 남겨두고 가끔씩만 크기를 조정하므로 평균 비용이 일정하게 유지됩니다). 항목을 하나씩 쌓아 리스트를 만드는 표준적인 방법입니다. insert()는 뒤따르는 원소들을 이동시켜야 하므로 O(n)입니다. extend()+=와 동등하며, 반복문에서 append()를 반복 호출하는 것보다 효율적입니다.

append()는 리스트가 여유 공간을 남겨두고 가끔씩만 크기를 다시 조정하기 때문에 비용이 적어서, 평균 비용이 O(1)(길이와 무관하게 일정)로 유지됩니다. insert(0, x)는 함정인데, 맨 앞에 삽입하면 모든 원소가 한 칸씩 오른쪽으로 밀려야 해서 O(n)이 됩니다. 맨 앞에 계속 추가해가며 리스트를 만든다면 전체적으로 O(n²)이 되어버립니다. 앞쪽 삽입이 빠르게 이루어져야 한다면 표준 라이브러리의 collections.deque를 쓰세요. 이것의 appendleft는 O(1)이며, 모듈 챕터에서 다룹니다. extend()는 리스트를 한 번에 확장하므로, 이미 가지고 있는 항목들을 넣을 때는 append()를 반복 호출하는 반복문보다 낫습니다.

python
scores = [87, 92, 74]

scores.append(65)          # [87, 92, 74, 65]
scores.insert(1, 100)      # [87, 100, 92, 74, 65]
scores.extend([55, 71])    # [87, 100, 92, 74, 65, 55, 71]

흔한 실수: 리스트를 append()하면 그 리스트 전체가 항목 하나로 추가되어, 리스트 안에 리스트가 들어가게 됩니다. 합치려면 대신 extend()를 쓰세요.

append(x)는 항상 x를 원소 하나로 추가합니다. append()에 리스트를 넘기면 중첩된 리스트가 됩니다. 다른 리스트의 모든 항목을 이 리스트에 합치고 싶을 때는 extend()를 쓰세요.

append(x)x가 무엇이든 원소 하나로 추가하므로, 리스트를 넘기면 그 리스트가 통째로 중첩됩니다. extend(iterable)은 인자를 순회하며 각 원소를 개별적으로 추가합니다. lst += otherextend와 같은 동작을 하므로, 호출부에서 더 명확히 읽히는 쪽을 쓰세요.

python
scores.append([55, 71])    # [..., [55, 71]]  중첩된 리스트, 대개 의도와 다름
scores.extend([55, 71])    # [..., 55, 71]    합쳐짐, 올바름
Juno항목 추가하기append()는 끝에 항목 하나를 붙이며 거의 항상 쓰게 될 방법입니다. insert()는 특정 위치에 항목을 넣고, extend()는 다른 리스트를 합쳐 넣습니다. 흔한 실수는 리스트를 append()하면 그 안에 리스트가 들어가버리는 것이니, 합치려면 extend()를 쓰세요.
Juno항목 추가하기append()가 기본이며, 항목을 하나씩 끝에 붙입니다. insert()는 그 뒤의 모든 항목을 옮겨야 해서 비용이 더 듭니다. 리스트를 append()하면 중첩되고, extend()(또는 +=)는 항목들을 대신 합쳐 넣습니다.
Juno항목 추가하기append()는 상각 O(1), insert(0, x)는 O(n)이므로, 반복문에서 앞쪽부터 계속 쌓다 보면 조용히 O(n²)이 되어버립니다. 그럴 때는 deque를 쓰세요. 그리고 리스트를 append()하면 통째로 중첩되니, extend()+=가 합치는 방법입니다.

항목 제거하기

항목을 제거하는 도구는 네 가지입니다. remove()는 값으로 검색합니다. pop()은 위치로 제거하며 그 항목을 돌려줍니다. del은 위치로 제거하며 반환값이 없습니다. clear()는 리스트 전체를 비웁니다.

remove()는 O(n)인데, 값으로 첫 번째로 일치하는 항목을 찾기 위해 훑어봅니다. pop()을 인자 없이 쓰면 마지막 항목에 대해 O(1)입니다. pop(i)로 다른 위치를 지정하면 원소들이 이동해야 해서 O(n)입니다. del scores[i]pop(i)와 동등하지만 반환값을 버립니다.

remove(value)는 앞에서부터 각 원소를 비교하며 일치하는 값을 찾은 뒤, 그 뒤의 모든 항목을 왼쪽으로 옮깁니다. 즉 O(n)입니다(비용이 리스트 길이에 따라 늘어남). 끝에서 pop()하는 것은 O(1)이고 아무것도 옮기지 않습니다. 다른 위치에서 pop(i)하는 것은 같은 이동 때문에 O(n)입니다. 자주 반복되는 루프에서 임의 위치의 항목을 제거하고 있다면, 반복되는 그 O(n)이 데이터 구조를 다시 짜거나 다른 컬렉션을 골라야 한다는 신호입니다.

python
scores = [87, 92, 74, 65, 91]

scores.remove(74)    # 74가 처음 나오는 위치를 제거
scores.pop()         # 마지막 항목(91)을 제거하고 반환
scores.pop(0)        # 0번 위치의 항목(87)을 제거하고 반환
del scores[1]        # 1번 위치를 제거, 반환값 없음
scores.clear()       # 전부 제거

값이 리스트에 없으면 remove()ValueError를 일으킵니다. 확실하지 않다면 먼저 in으로 확인하세요.

python
if 74 in scores:
    scores.remove(74)

값을 찾지 못하면 remove()ValueError를 일으킵니다. in으로 확인하면 O(n) 스캔이 한 번 더 추가되어 리스트를 두 번 훑게 됩니다. 한 번 쓰고 마는 코드라면 문제없습니다. 미리 확인하는 대신 실패를 잡아내고 싶다면, try / except ValueError파일과 예외 챕터에서 다룹니다.

in으로 확인한 뒤 remove()하는 패턴은 O(n) 스캔이 두 번, 즉 리스트를 두 번 훑는 것입니다. 순서가 중요하지 않다면, 제거할 대상을 마지막 원소와 바꾸고 pop()하면 O(1)입니다(이동 없이 일정한 비용). 그리고 소속 여부를 자주 검사한다면, set은 O(1)로 조회하는 반면 리스트는 O(n)입니다. 이는 튜플과 세트 챕터에서 다룹니다. 원칙은 이렇습니다: 순서가 있고 계속 바뀌는 데이터에는 리스트를, "그 안에 있는지"가 핵심 질문이 되는 순간에는 세트를 쓰세요.

Juno항목 제거하기remove()는 값으로 지우되 처음 일치하는 것만 지우고, 값이 없으면 ValueError를 일으키므로 확실하지 않을 때는 먼저 in으로 확인하세요. pop()은 위치로 제거하고 그 항목을 돌려주며, del은 위치로 제거하고 아무것도 돌려주지 않습니다.
Juno항목 제거하기remove()는 값이 처음 일치하는 것을 지우고, 값을 찾지 못하면 ValueError를 일으킵니다. 끝에서 pop()하는 것은 비용이 적고 항목을 돌려주지만, 다른 곳에서 pop(i)하면 그 뒤 전부를 옮겨야 합니다. del은 위치로 제거하며 반환값이 없습니다.
Juno항목 제거하기 끝이 아닌 위치의 remove()pop(i)는 O(n) 이동이 필요하고, 끝에서 pop()하는 것만 O(1)입니다. "그 안에 있는지"가 핵심적으로 자주 검사하는 경로라면 set이 O(1)로 처리하는 반면 리스트는 O(n)입니다. remove() 전에 in으로 미리 확인하면 리스트를 두 번 훑게 됩니다.

정렬

sorted()는 완전히 새로운 정렬된 리스트를 돌려주고 원본은 그대로 둡니다. .sort()는 리스트 자체를 정렬하고 None을 반환합니다. 이 차이는 보이는 것보다 훨씬 중요합니다.

sorted()는 안전한 기본 선택입니다. 원본을 절대 바꾸지 않습니다. .sort()는 그 자리에서 바꾸고 None을 반환하는데, 이는 흔히 빠지는 함정입니다. .sort()의 결과를 변수에 대입하면 정렬된 리스트가 아니라 None을 얻게 됩니다. 원본을 그대로 두어야 할 때는 sorted()를, 정렬된 결과만 필요할 때는 .sort()를 쓰세요.

파이썬의 정렬은 **안정적(stable)**입니다. 같은 값의 원소들은 원래의 상대적 순서를 유지합니다. 그래서 한 기준으로 정렬한 뒤 다른 기준으로 다시 정렬해도, 앞서의 순서가 동점을 가르는 기준으로 살아남습니다. 이미 거의 정렬된 데이터에 대해서도 빠르게 동작해서, 처음부터 정렬하는 O(n log n)보다는 O(n)(선형)에 가까우므로, 대체로 정렬된 리스트를 다시 정렬하는 것은 비용이 적습니다. .sort()None을 반환하는 것은 의도적인 설계입니다. 리스트를 그 자리에서 바꾸는 메서드는 그것을 다시 반환하지 않는다는 관례인데, 이는 x = lst.sort()를 써놓고 조용히 None을 얻는 실수를 막기 위한 것입니다. sorted()는 리스트뿐 아니라 어떤 이터러블이든 받을 수 있고 항상 새 리스트를 돌려줍니다.

python
scores = [87, 42, 96, 55, 71]

ranked = sorted(scores)            # [42, 55, 71, 87, 96] (새 리스트)
scores.sort()                      # 그 자리에서 정렬, None 반환
scores.sort(reverse=True)          # [96, 87, 71, 55, 42]

result = scores.sort()             # result는 None, 정렬된 리스트가 아님
Juno정렬sorted()는 새로운 정렬된 리스트를 주고 원본은 그대로 둡니다. .sort()는 리스트 자체를 재배열하고 None을 반환하니, x = scores.sort()를 쓰면 리스트가 아니라 None을 얻게 됩니다. 저도 이 실수를 몸에 익을 때까지 한 번 이상 저질렀습니다.
Juno정렬sorted()가 안전한 기본값입니다. 원본을 절대 건드리지 않습니다. .sort()는 그 자리에서 정렬하고 None을 반환하니, 그 결과를 대입하는 것이 기억해야 할 함정입니다. 원본을 그대로 둬야 할 때는 sorted()를 고르세요.
Juno정렬 정렬은 안정적이라서, 보조 기준으로 먼저 정렬한 뒤 주요 기준으로 정렬하면 동점은 이전 순서를 유지합니다. 거의 정렬된 데이터에서는 거의 선형으로 동작해서 재정렬이 저렴합니다. .sort()는 의도적으로 None을 반환하고, sorted()는 어떤 이터러블이든 받아 새 리스트를 돌려줍니다.

유용한 연산들

파이썬에는 리스트에 직접 쓸 수 있는 내장 도구들이 있습니다. len(), sum(), min(), max()가 앞으로 계속 쓰게 될 네 가지입니다.

내장 시퀀스 함수들은 어떤 리스트에서든 동작합니다. in은 리스트에서는 선형 탐색이므로, 소속 여부를 반복적으로 빠르게 검사해야 한다면 세트로 변환하세요. .index()는 값을 찾지 못하면 ValueError를 일으킵니다.

len(), sum(), min(), max()는 리스트뿐 아니라 어떤 이터러블에서든 동작해서, 같은 호출로 리스트든 튜플이든 제너레이터든 읽을 수 있습니다. 중요한 비용 관련 사항은 이렇습니다. in.index()는 둘 다 앞에서부터 훑기 때문에 O(n)입니다(비용이 길이에 따라 늘어남). 리스트는 내용에 대한 색인을 따로 가지고 있지 않기 때문입니다. 반복문에서 소속 여부를 반복적으로 검사하면 이 비용이 쌓이는데, 이때의 해결책은 set입니다. 한 가지 헷갈리는 부분: sum()0에서 시작하므로 숫자를 더할 뿐 문자열은 더하지 못합니다. 문자열을 이어붙이려면 "".join()을 쓰세요. 이는 문자열 챕터에서 다룹니다.

python
scores = [87, 92, 74, 65, 91]

len(scores)          # 5
sum(scores)          # 409
min(scores)          # 65
max(scores)          # 92
scores.count(87)     # 1
scores.index(74)     # 2
74 in scores         # True
74 not in scores     # False
scores.copy()        # 얕은 복사
scores.reverse()     # 그 자리에서 뒤집기
Juno유용한 연산들len(), sum(), min(), max() 모두 별도의 준비 없이 리스트에 바로 쓸 수 있습니다. in은 무언가가 있는지 묻고, .count()는 몇 번 나오는지 세며, .index()는 처음 일치하는 위치를 찾습니다.
Juno유용한 연산들 내장 함수들은 어떤 리스트에서든 동작하고, 시퀀스 관련 함수들은 어떤 이터러블에서든 동작합니다. in은 선형 탐색이므로, 소속 여부를 반복적으로 검사할 때는 세트로 바꾸세요. .index()는 값을 찾지 못하면 ValueError를 일으킵니다.
Juno유용한 연산들len(), sum(), min(), max()는 리스트뿐 아니라 어떤 이터러블이든 받습니다. in.index()는 둘 다 O(n) 스캔이라서, 소속 여부를 자주 검사하는 경로라면 set이 필요합니다. 그리고 sum()0부터 시작하므로 문자열에는 "".join()을 쓰세요.

순회하기

for 반복문은 리스트를 한 번에 한 항목씩 훑습니다. for 뒤의 변수는 매 차례마다 항목을 하나씩 받습니다. 위치도 함께 필요하다면, enumerate()가 수동으로 카운터를 만들지 않아도 둘 다 알려줍니다.

for item in list는 리스트의 이터레이터를 호출해 매 단계마다 앞으로 진행시킵니다. enumerate(iterable, start=0)은 이터레이터를 감싸서 (index, value) 쌍을 만들어냅니다. 카운터 변수를 직접 관리하는 것보다 enumerate()를 쓰는 것이 더 깔끔하고 실수도 덜 생깁니다.

enumerate()는 반복하는 동안 (index, value) 쌍을 만들어내는데, 그래서 for i, item in enumerate(...)가 깔끔하게 읽히는 겁니다. 각 쌍이 두 이름으로 언패킹되기 때문입니다. start 인자는 표시용 카운터만 옮겨줄 뿐입니다(예를 들어 start=1은 1부터 번호를 붙입니다). 반복문이 리스트에서 실제로 읽는 위치는 바뀌지 않습니다. 손으로 관리하는 카운터보다 이걸 선호하는 이유는 속도가 아니라, 직접 증가시켜야 하는 별도의 카운터는 깜빡하고 안 늘릴 여지가 하나 더 생기는데 enumerate()는 어긋날 일이 없다는 점입니다.

python
players = ["민준", "서연", "지호"]

for player in players:
    print(player)

for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")
# 1. 민준
# 2. 서연
# 3. 지호

forenumerate()제어 흐름 챕터에서 자세히 다룹니다. 짧게 정리하면: for player in players는 항목마다 한 번씩 실행되고, enumerate()는 매 반복마다 위치와 값을 동시에 줍니다.

Juno순회하기for 반복문은 리스트를 한 번에 한 항목씩 훑고, for 뒤의 이름은 매 차례 그 항목이 됩니다. 위치도 필요하다면 enumerate()가 둘 다 넘겨주니, 카운터를 직접 관리할 필요가 없습니다. 대신 세어주게 하니까 오프바이원(off-by-one) 실수를 여러 번 피할 수 있었습니다.
Juno순회하기for item in list는 각 항목을 순서대로 방문합니다. enumerate(iterable, start=0)(index, value) 쌍을 만들어내는데, 손으로 관리하는 카운터보다 깔끔하고 실수도 덜합니다. 1부터 번호를 매기고 싶다면 start=1을 쓰세요.
Juno순회하기enumerate()(index, value) 쌍을 만들어 바로 두 이름으로 언패킹되고, start는 표시되는 카운터만 옮길 뿐 실제로 읽는 위치는 그대로입니다. 이득은 속도가 아니라, 수동 카운터는 어긋날 수 있지만 이건 그럴 수 없다는 점입니다.

중첩 리스트

리스트는 다른 리스트를 담을 수 있습니다. 이것이 격자나 표를 표현하는 방법입니다. 행들의 리스트이며, 각 행은 값들의 리스트입니다. 대괄호를 두 번 써서 항목에 접근합니다. 첫 번째는 행을 고르고, 두 번째는 열을 고릅니다.

중첩 리스트는 리스트에 대한 참조들의 리스트입니다. 각 안쪽 리스트는 독립된 객체입니다. grid[row][col]처럼 대괄호를 연결해 접근합니다. 안쪽 리스트를 변경하면 바깥쪽 리스트에도 영향이 미치는데, 바깥쪽 리스트가 같은 객체에 대한 참조를 담고 있기 때문입니다.

중첩 리스트는 진짜 2차원 배열이 아닙니다. 바깥쪽 리스트는 안쪽 리스트들에 대한 참조를 담고 있고, 각각은 독립된 객체이므로 행마다 길이가 다르거나 타입이 달라도 됩니다. grid[1][2]는 두 번의 조회입니다. 행을 고른 뒤 그 안에서 인덱싱하는 것입니다. 사람들이 자주 걸려드는 결과는 이렇습니다. 바깥쪽 리스트의 얕은 복사(.copy()가 만드는 종류)는 바깥쪽 컨테이너는 복제하지만 여전히 같은 안쪽 리스트들을 가리키므로, 안쪽 행을 바꾸면 양쪽 복사본 모두에 나타납니다. 아래 가변성 섹션에서 이를 구체적으로 살펴봅니다.

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

grid[0]       # [1, 2, 3]
grid[1][2]    # 6  (1행, 2열)
Juno중첩 리스트 리스트는 다른 리스트를 담을 수 있는데, 이것이 격자나 표를 만드는 방법입니다. 행들의 리스트이고 각 행이 하나의 리스트입니다. 대괄호를 두 번 써서 안으로 들어갑니다. grid[1][2]에서 첫 번째는 행을 고르고 두 번째는 그 안에서 고릅니다.
Juno중첩 리스트 중첩 리스트는 다른 리스트에 대한 참조들의 리스트이며, 각 안쪽 리스트는 자기 자신의 객체입니다. 대괄호를 연결해 grid[row][col]처럼 접근하세요. 바깥쪽 리스트가 참조만 담고 있기 때문에, 안쪽 리스트를 바꾸면 그것을 가리키는 모든 이름을 통해 드러납니다.
Juno중첩 리스트 진짜 2차원 배열은 아닙니다. 바깥쪽 리스트는 참조를 담고 있어서 안쪽 행들의 길이와 타입이 서로 달라도 되고, grid[1][2]는 두 번의 조회입니다. 얕은 복사는 바깥쪽 리스트는 복제하지만 안쪽 리스트는 공유하는데, 이것이 다음 섹션에서 다루는 함정입니다.

가변성: 함정

거의 모두가 여기서 놀랍니다. 리스트를 새 변수에 대입하는 것은 복사가 아닙니다. 두 이름 모두 같은 리스트를 가리킵니다. 하나를 바꾸면 다른 것도 바뀝니다. 독립된 복사본을 얻으려면 명시적으로 요청해야 합니다.

리스트 대입은 객체가 아니라 참조를 복사합니다. 두 이름 모두 같은 리스트를 가리킵니다. 어느 쪽 이름으로 변경하든 같은 데이터에 영향을 미칩니다. 독립된 데이터가 필요할 때는 .copy(), list(), 또는 전체 슬라이스 [:]로 명시적으로 복사하세요.

b = a는 두 번째 이름을 같은 리스트 객체에 향하게 하므로, b를 통한 어떤 변경이든 a가 보는 그 객체에도 그대로 나타납니다. .copy()a[:]얕은 복사를 만듭니다. 원본이 가지고 있던 것과 같은 원소 참조들을 담은, 새로운 바깥쪽 리스트입니다. 변하지 않는(immutable) 값들(숫자, 문자열처럼 자리에서 바뀔 수 없는 것들)로만 이루어진 평평한 리스트라면 이것만으로 충분합니다. 중첩 리스트에서는 함정이 됩니다. 안쪽 리스트들이 여전히 원본과 복사본 사이에 공유되어 있어서, 한쪽을 통해 행을 수정하면 다른 쪽에도 나타나기 때문입니다.

python
a = [1, 2, 3]
b = a            # b는 복사본이 아니라 같은 리스트를 가리킴

b.append(4)
print(a)         # [1, 2, 3, 4]  (바뀜: a와 b는 같은 리스트)
python
b = a.copy()    # 독립된 복사본
b = list(a)     # 결과는 같음
b = a[:]        # 이것도 마찬가지

# 중첩 리스트는 여전히 안쪽 객체를 공유합니다:
matrix = [[1, 2], [3, 4]]
copy = matrix.copy()

copy[0].append(99)
print(matrix)   # [[1, 2, 99], [3, 4]]  (안쪽 리스트가 공유됨)

완전히 독립적인 상태가 필요한 중첩 구조라면 안쪽 리스트를 하나씩 직접 복사하거나, 표준 라이브러리의 copy.deepcopy()를 쓰세요. 이는 모듈 챕터에서 다룹니다.

Juno가변성: 함정b = a는 복사본을 만들지 않습니다. 두 이름 모두 같은 리스트를 가리키므로, 하나를 통해 바꾸면 다른 쪽에도 드러납니다. 독립된 리스트를 원한다면 명시적으로 요청해야 합니다: .copy(), list(a), 또는 a[:]. 이건 처음에는 거의 모두를 놀라게 하는데, 저도 그랬습니다.
Juno가변성: 함정 대입은 리스트가 아니라 참조를 복사하므로, b = a는 하나의 객체에 두 이름을 남기고 어느 쪽으로 변경하든 같은 데이터를 건드립니다. 독립성이 필요할 때는 .copy(), list(a), 또는 a[:]로 명시적으로 복사하세요.
Juno가변성: 함정b = a는 하나의 객체를 공유하고, .copy()a[:]는 같은 안쪽 참조들 위에 새 바깥쪽 리스트를 만드는 얕은 복사입니다. 변하지 않는 값들로만 이루어진 평평한 리스트는 그렇게 해도 안전하지만, 중첩된 리스트는 행을 공유하니, 안쪽까지 독립적이어야 할 때는 copy.deepcopy()를 쓰세요.

더 많은 메서드

메서드동작
.append(item)끝에 추가
.insert(i, item)i 위치에 삽입
.extend(iterable)이터러블의 모든 항목을 추가
.remove(value)값이 처음 나오는 항목을 제거
.pop(i)i 위치의 항목을 제거하고 반환 (기본값: 마지막)
.clear()모든 항목 제거
.index(value)값이 처음 나오는 위치
.count(value)값이 나오는 횟수
.sort()그 자리에서 정렬
.reverse()그 자리에서 뒤집기
.copy()얕은 복사본 반환

실전 예제

점수 추적기를 만들어봅니다. 결과를 추가하고, 정렬하고, 요약을 출력합니다.

python
scores = []

scores.append(87)
scores.append(54)
scores.append(92)
scores.append(67)
scores.append(45)

scores.sort(reverse=True)

print(f"순위별 점수: {scores}")
print(f"최고점: {scores[0]}")
print(f"최저점: {scores[-1]}")
print(f"평균: {sum(scores) / len(scores):.1f}")
print(f"상위 3명: {scores[:3]}")

이름과 점수, 두 개의 병렬 리스트로 최고 성적자를 찾고 순위별 결과를 출력합니다.

python
names = ["민준", "서연", "지호", "하윤"]
scores = [87, 74, 92, 55]

best_score = max(scores)
best_index = scores.index(best_score)
best_player = names[best_index]

print(f"최고 성적자: {best_player} ({best_score})")
print(f"평균:        {sum(scores) / len(scores):.1f}")

ranked = sorted(scores, reverse=True)
print(f"분포 (순위별): {ranked}")

for rank, score in enumerate(ranked, start=1):
    print(f"  {rank}위: {score}")

별칭(aliasing)과 복사의 차이, 그리고 중첩 리스트의 얕은 복사와 깊은 복사의 차이를 보여줍니다.

python
# 별칭: b는 복사본이 아님
a = [1, 2, 3]
b = a
b.append(4)
print(a)    # [1, 2, 3, 4]  (같은 객체)

# 얕은 복사: 바깥쪽 리스트는 독립적이지만 안쪽 리스트는 공유됨
matrix = [[1, 2, 3], [4, 5, 6]]
shallow = matrix.copy()
shallow[0].append(99)
print(matrix)    # [[1, 2, 3, 99], [4, 5, 6]]  (안쪽 리스트가 공유됨)

# for 반복문으로 직접 깊은 복사 (import 불필요)
matrix = [[1, 2, 3], [4, 5, 6]]
deep_copy = []
for row in matrix:
    deep_copy.append(row[:])    # 각 안쪽 리스트를 명시적으로 복사

deep_copy[0].append(99)
print(matrix)    # [[1, 2, 3], [4, 5, 6]]  (변하지 않음)