리스트
변수는 한 가지만 담습니다. 리스트는 여러 가지를 순서대로, 하나의 이름 아래 담습니다. 리더보드는 순위가 매겨진 점수들의 나열이고, 퀴즈는 문제들의 모음입니다. 관련된 값들의 묶음을 관리해야 하는 순간부터 리스트가 필요해집니다.
리스트 만들기
대괄호 안에 값을 쉼표로 구분해 나열합니다. 리스트는 어떤 타입이든 섞어서 담을 수 있고, 빈 리스트도 유효하며 시간을 두고 채워나가는 시작점으로 흔히 쓰입니다.
scores = [87, 92, 74, 65, 91]
players = ["민준", "서연", "지호"]
mixed = ["민준", 87, True, 3.14] # 어떤 타입이든 되지만 흔치 않음
empty = [][]로 시작해도 좋습니다. 제 리스트들도 대부분 그렇게, 비어 있는 채로 기다리며 시작합니다. 인덱싱과 슬라이싱
리스트는 문자열과 같은 번호 체계를 씁니다. 위치는 0부터 시작하고, 음수는 끝에서부터 셉니다. 위치를 지정해 어떤 항목이든 읽을 수 있습니다. 리스트는 변경 가능하므로 특정 위치에 값을 쓸 수도 있습니다.
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 발생)0부터 시작하고, 음수는 끝에서부터 거꾸로 세므로 scores[-1]이 마지막 항목입니다. scores[1:3] 같은 슬라이스는 1번과 2번 위치를 담은 새 리스트를 돌려줍니다. 잠깐 걸렸던 부분: 리스트는 위치에 값을 쓸 수도 있다는 것, scores[0] = 90처럼요. 문자열이라면 거부당했을 겁니다. 항목 추가하기
항목을 추가하는 방법은 세 가지입니다. append()는 끝에 항목 하나를 추가하며, 거의 항상 쓰게 될 방법입니다. insert()는 특정 위치에 추가합니다. extend()는 다른 리스트를 합쳐 넣습니다.
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()를 쓰세요.
scores.append([55, 71]) # [..., [55, 71]] 중첩된 리스트, 대개 의도와 다름
scores.extend([55, 71]) # [..., 55, 71] 합쳐짐, 올바름append()는 끝에 항목 하나를 붙이며 거의 항상 쓰게 될 방법입니다. insert()는 특정 위치에 항목을 넣고, extend()는 다른 리스트를 합쳐 넣습니다. 흔한 실수는 리스트를 append()하면 그 안에 리스트가 들어가버리는 것이니, 합치려면 extend()를 쓰세요. 항목 제거하기
항목을 제거하는 도구는 네 가지입니다. remove()는 값으로 검색합니다. pop()은 위치로 제거하며 그 항목을 돌려줍니다. del은 위치로 제거하며 반환값이 없습니다. clear()는 리스트 전체를 비웁니다.
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으로 확인하세요.
if 74 in scores:
scores.remove(74)remove()는 값으로 지우되 처음 일치하는 것만 지우고, 값이 없으면 ValueError를 일으키므로 확실하지 않을 때는 먼저 in으로 확인하세요. pop()은 위치로 제거하고 그 항목을 돌려주며, del은 위치로 제거하고 아무것도 돌려주지 않습니다. 정렬
sorted()는 완전히 새로운 정렬된 리스트를 돌려주고 원본은 그대로 둡니다. .sort()는 리스트 자체를 정렬하고 None을 반환합니다. 이 차이는 보이는 것보다 훨씬 중요합니다.
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, 정렬된 리스트가 아님sorted()는 새로운 정렬된 리스트를 주고 원본은 그대로 둡니다. .sort()는 리스트 자체를 재배열하고 None을 반환하니, x = scores.sort()를 쓰면 리스트가 아니라 None을 얻게 됩니다. 저도 이 실수를 몸에 익을 때까지 한 번 이상 저질렀습니다. 유용한 연산들
파이썬에는 리스트에 직접 쓸 수 있는 내장 도구들이 있습니다. len(), sum(), min(), max()가 앞으로 계속 쓰게 될 네 가지입니다.
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() # 그 자리에서 뒤집기len(), sum(), min(), max() 모두 별도의 준비 없이 리스트에 바로 쓸 수 있습니다. in은 무언가가 있는지 묻고, .count()는 몇 번 나오는지 세며, .index()는 처음 일치하는 위치를 찾습니다. 순회하기
for 반복문은 리스트를 한 번에 한 항목씩 훑습니다. for 뒤의 변수는 매 차례마다 항목을 하나씩 받습니다. 위치도 함께 필요하다면, enumerate()가 수동으로 카운터를 만들지 않아도 둘 다 알려줍니다.
players = ["민준", "서연", "지호"]
for player in players:
print(player)
for i, player in enumerate(players, start=1):
print(f"{i}. {player}")
# 1. 민준
# 2. 서연
# 3. 지호for와 enumerate()는 제어 흐름 챕터에서 자세히 다룹니다. 짧게 정리하면: for player in players는 항목마다 한 번씩 실행되고, enumerate()는 매 반복마다 위치와 값을 동시에 줍니다.
for 반복문은 리스트를 한 번에 한 항목씩 훑고, for 뒤의 이름은 매 차례 그 항목이 됩니다. 위치도 필요하다면 enumerate()가 둘 다 넘겨주니, 카운터를 직접 관리할 필요가 없습니다. 대신 세어주게 하니까 오프바이원(off-by-one) 실수를 여러 번 피할 수 있었습니다. 중첩 리스트
리스트는 다른 리스트를 담을 수 있습니다. 이것이 격자나 표를 표현하는 방법입니다. 행들의 리스트이며, 각 행은 값들의 리스트입니다. 대괄호를 두 번 써서 항목에 접근합니다. 첫 번째는 행을 고르고, 두 번째는 열을 고릅니다.
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
grid[0] # [1, 2, 3]
grid[1][2] # 6 (1행, 2열)grid[1][2]에서 첫 번째는 행을 고르고 두 번째는 그 안에서 고릅니다. 가변성: 함정
거의 모두가 여기서 놀랍니다. 리스트를 새 변수에 대입하는 것은 복사가 아닙니다. 두 이름 모두 같은 리스트를 가리킵니다. 하나를 바꾸면 다른 것도 바뀝니다. 독립된 복사본을 얻으려면 명시적으로 요청해야 합니다.
a = [1, 2, 3]
b = a # b는 복사본이 아니라 같은 리스트를 가리킴
b.append(4)
print(a) # [1, 2, 3, 4] (바뀜: a와 b는 같은 리스트)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]] (안쪽 리스트가 공유됨)b = a는 복사본을 만들지 않습니다. 두 이름 모두 같은 리스트를 가리키므로, 하나를 통해 바꾸면 다른 쪽에도 드러납니다. 독립된 리스트를 원한다면 명시적으로 요청해야 합니다: .copy(), list(a), 또는 a[:]. 이건 처음에는 거의 모두를 놀라게 하는데, 저도 그랬습니다. 더 많은 메서드
| 메서드 | 동작 |
|---|---|
.append(item) | 끝에 추가 |
.insert(i, item) | i 위치에 삽입 |
.extend(iterable) | 이터러블의 모든 항목을 추가 |
.remove(value) | 값이 처음 나오는 항목을 제거 |
.pop(i) | i 위치의 항목을 제거하고 반환 (기본값: 마지막) |
.clear() | 모든 항목 제거 |
.index(value) | 값이 처음 나오는 위치 |
.count(value) | 값이 나오는 횟수 |
.sort() | 그 자리에서 정렬 |
.reverse() | 그 자리에서 뒤집기 |
.copy() | 얕은 복사본 반환 |
실전 예제
점수 추적기를 만들어봅니다. 결과를 추가하고, 정렬하고, 요약을 출력합니다.
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]}")
