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

리스트

변수는 하나의 값을 담습니다. 리스트는 여러 값을 순서대로, 하나의 이름 아래 담습니다. 리더보드는 순위가 매겨진 점수들의 나열입니다. 퀴즈는 질문들의 모음입니다. 관련된 값들의 그룹을 다루어야 할 때, 리스트가 필요합니다.

리스트는 파이썬의 범용 순서가 있고 가변적인 시퀀스입니다. 시간이 지남에 따라 변하는 모든 것에 자연스럽게 어울립니다: 항목이 추가되거나 제거되고, 순서가 섞이고, 내용이 필터링되거나 정렬되는 경우 말이죠. 순서가 중요하고 컬렉션이 변할 때, 리스트가 보통 첫 번째 선택입니다.

list는 파이썬의 동적 배열입니다: 연속된 힙 할당으로 뒷받침되는 순서가 있고 가변적인 시퀀스입니다. 임의 접근은 O(1)입니다. append()는 배열이 초과 할당되고 오버플로 시 확장되기 때문에 분할 상환 O(1)입니다. insert()remove()는 이후 요소들을 이동시키기 때문에 O(n)입니다. 이러한 비용은 다른 구조를 선호할 시점을 결정하는 데 지침이 되어야 합니다.

리스트 생성하기

대괄호 안에 값들을 쉼표로 구분합니다. 리스트는 어떤 타입의 조합도 담을 수 있으며, 빈 리스트는 유효하고 시간이 지나면서 채워 나갈 시작점으로 흔히 사용됩니다.

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

대괄호 리터럴은 사전 할당된 용량을 가진 새 list 객체를 힙에 할당합니다. 요소는 어떤 파이썬 객체든 될 수 있습니다; 리스트는 값을 직접 저장하지 않고 참조를 저장합니다. 이질적인 요소 타입은 유효하지만 빠른 스크립트 외에는 실무에서 드뭅니다.

python
scores   = [87, 92, 74, 65, 91]
players  = ["민준", "서연", "지호"]
mixed    = ["민준", 87, True, 3.14]   # 모든 타입 가능, 다만 드묾
empty    = []

인덱싱과 슬라이싱

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

리스트 인덱싱과 슬라이싱은 문자열과 동일한 규칙을 따릅니다. 주요 차이점은 가변성입니다: 인덱스나 슬라이스에 할당해서 항목을 그 자리에서 변경할 수 있는데, 문자열은 이를 허용하지 않습니다.

list.__getitem__str과 같은 클램핑 규칙을 따라 정수와 slice 객체를 받습니다. __setitem__은 항목 할당을 가능하게 합니다. 슬라이스 할당은 요소 범위를 교체하며, 교체 항목의 길이가 다르면 리스트 크기를 조정할 수 있습니다: lst[1:3] = [10, 20, 30]은 두 항목을 세 항목으로 교체합니다.

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 발생)

항목 추가하기

항목을 추가하는 세 가지 방법이 있습니다. append()는 끝에 단일 항목을 추가하며 거의 항상 사용하게 될 메서드입니다. insert()는 특정 위치에 추가합니다. extend()는 다른 리스트를 병합합니다.

append()는 분할 상환 O(1)이며 항목을 하나씩 리스트로 구축하는 표준 방법입니다. insert()는 이후 요소를 이동시키기 때문에 O(n)입니다. extend()+=와 동등하며 루프 안에서 append()를 반복하는 것보다 효율적입니다.

append()는 사전 할당된 버퍼를 사용하며 오버플로가 크기 조정을 유발할 때만 복사합니다. 처음 몇 번의 크기 조정 이후 증가 계수는 대략 1.125배이며, 분할 상환 O(1)을 제공합니다. insert(0, x)는 O(n)입니다: 모든 요소가 오른쪽으로 이동합니다. 앞쪽 삽입이 빈번하다면 collections.deque가 O(1) appendleft를 제공합니다. extend(iterable)__iter__를 한 번 호출하고 한 번의 작업으로 확장됩니다.

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를 단일 객체로 list_append를 호출합니다. extend(iterable)은 인자에 __iter__를 호출하고 각 요소를 개별적으로 추가합니다. += 연산자는 __iadd__를 호출하며, 내부적으로 extend를 호출합니다.

python
scores.append([55, 71])    # [..., [55, 71]]  중첩 리스트, 의도와 다를 가능성 큼
scores.extend([55, 71])    # [..., 55, 71]    병합됨, 올바름

항목 제거하기

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

remove()는 O(n)입니다: 값으로 첫 번째 발생을 스캔합니다. 인자 없는 pop()은 마지막 항목에 대해 O(1)입니다. 다른 위치의 pop(i)는 요소가 이동하기 때문에 O(n)입니다. del scores[i]pop(i)와 동등하지만 반환값을 버립니다.

remove(value)는 일치하는 항목을 찾을 때까지 각 요소에 __eq__를 호출하고, 그 후 모든 이후 요소를 왼쪽으로 이동시킵니다: O(n). pop(-1)은 O(1)로, 이동이 필요 없습니다. 다른 인덱스의 pop(i)는 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). O(1) 조회로 집합 멤버십을 원한다면 list 대신 set을 사용하세요. 튜플, 집합 장에서 다룹니다.

정렬

sorted()는 새로 정렬된 리스트를 반환하고 원본은 그대로 둡니다. .sort()는 리스트를 그 자리에서 정렬하고 None을 반환합니다. 이 차이는 들리는 것보다 훨씬 중요합니다.

sorted()는 안전한 기본 선택입니다: 원본을 절대 수정하지 않습니다. .sort()는 그 자리에서 수정하고 None을 반환하는데, 이는 흔한 함정입니다. .sort()의 결과를 할당하면 정렬된 리스트가 아니라 None이 됩니다. 원본을 유지해야 할 때는 sorted()를 사용하고, 정렬된 버전만 필요할 때는 .sort()를 사용하세요.

둘 다 Timsort를 사용합니다: 병합/삽입 정렬의 하이브리드로, 최악의 경우 O(n log n)이며 거의 정렬된 데이터에서는 O(n)입니다. Timsort는 안정적입니다: 같은 요소는 원래의 상대 순서를 유지합니다. .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

유용한 연산

파이썬에는 리스트에서 직접 동작하는 내장 도구들이 있습니다. len(), sum(), min(), max()는 끊임없이 사용하게 될 네 가지입니다.

내장 시퀀스 함수들은 어떤 리스트에서든 동작합니다. 리스트에서 in은 선형 스캔입니다; 빠른 반복 멤버십 검사가 필요하다면 집합으로 변환하세요. .index()는 값을 찾지 못하면 ValueError를 발생시킵니다.

len(), sum(), min(), max() 모두 __iter__를 호출하며 리스트뿐만 아니라 어떤 이터러블에서도 동작합니다. 리스트에서 in은 O(n)이고, O(1) 조회를 원하면 set을 사용하세요. .index(value)도 O(n)입니다. sum()은 기본적으로 start=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()     # 그 자리에서 역순으로

반복

for 루프는 리스트를 한 번에 한 항목씩 순회합니다. for 뒤의 변수가 각 항목을 차례대로 받습니다. 위치도 함께 필요할 때는 enumerate()가 수동 카운터 없이 둘 다 제공합니다.

for item in list은 리스트의 이터레이터를 호출하고 각 단계에서 진행시킵니다. enumerate(iterable, start=0)는 이터레이터를 감싸고 (index, value) 쌍을 생성합니다. enumerate()를 사용하는 것이 카운터 변수를 유지하는 것보다 깔끔하고 오류가 덜 발생합니다.

foriter(list)를 호출해 list_iterator를 얻고, StopIteration이 발생할 때까지 next()를 호출합니다. enumerate()는 어떤 이터레이터든 감싸고 (i, value) 쌍을 생성합니다. start 매개변수는 카운터를 오프셋하지만 기본 인덱스에는 영향을 주지 않습니다. enumerate가 튜플을 생성하기 때문에 i, item = pair 언패킹이 작동합니다.

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

for player in players:
    print(player)

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

for 루프와 enumerate

forenumerate()제어 흐름 장에서 완전히 다룹니다. 짧게 말해: for player in players는 각 항목당 한 번 실행되며, enumerate()는 매 반복마다 위치와 값을 모두 제공합니다.

중첩 리스트

리스트는 다른 리스트를 포함할 수 있습니다. 이는 그리드나 테이블을 표현하는 방법입니다: 행들의 리스트이며, 각 행은 값들의 리스트입니다. 두 쌍의 대괄호로 항목에 접근합니다: 첫 번째는 행을 선택하고, 두 번째는 열을 선택합니다.

중첩 리스트는 리스트 참조의 리스트입니다. 각 내부 리스트는 독립적인 객체입니다. 연쇄적인 첨자로 접근합니다: grid[row][col]. 외부 리스트가 같은 객체에 대한 참조를 보유하기 때문에 내부 리스트를 변경하면 외부 리스트에도 영향을 미칩니다.

중첩 리스트는 진정한 2D 배열이 아닙니다: 외부 리스트는 객체 참조를 보유하며, 내부 리스트는 길이와 타입이 다를 수 있습니다. 접근은 두 번의 __getitem__ 호출을 연쇄합니다. 중첩 리스트의 얕은 복사는 외부 컨테이너만 복사하고 내부 리스트는 복사하지 않습니다; 내부 리스트에 대한 수정은 두 복사본 모두에 영향을 미칩니다.

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

grid[0]       # [1, 2, 3]
grid[1][2]    # 6  (행 1, 열 2)

가변성: 함정

이는 거의 모든 사람을 놀라게 합니다. 리스트를 새 변수에 할당해도 복사본이 만들어지지 않습니다. 두 이름 모두 같은 리스트를 가리킵니다. 하나를 변경하면 다른 것도 변경됩니다. 독립적인 복사본을 얻으려면 명시적으로 요청해야 합니다.

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

b = a는 두 번째 이름을 같은 리스트 객체에 바인딩합니다. b를 통한 모든 변경은 a도 가리키는 객체를 변경합니다. .copy()a[:]얕은 복사를 생성합니다: 같은 요소 참조를 가진 새 리스트 객체. 불변 값의 평평한 리스트에는 안전하지만, 중첩 리스트의 경우 내부 객체는 여전히 공유됩니다.

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()를 사용하세요. 모듈 장에서 다룹니다.

추가 메서드

메서드하는 일
.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 i in range(len(ranked)):
    print(f"  순위 {i + 1}: {ranked[i]}")

별칭(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]]  (변경 없음)