프로그래밍

리스트

변수 하나에 값 하나씩 저장하다 보면
서른 명의 점수에서 막힙니다.

학습 목표

  1. 여러 값을 리스트 하나에 저장하고, 요소의 개수를 구할 수 있다.
  2. 인덱스로 값을 꺼내고 바꿀 수 있으며, 왜 0부터 세는지 설명할 수 있다.
  3. 요소를 추가하거나 삭제하고, 리스트의 일부를 잘라 낼 수 있다.
  4. 합계·최댓값·정렬처럼 리스트 전체를 한 번에 다루는 방법을 쓸 수 있고, 리스트에 이름을 하나 더 붙였을 때 무슨 일이 생기는지 안다.

값 여러 개를 한 이름에

변수를 서른 개 만들 수는 없습니다.

학생 네 명의 점수를 변수 하나에 값 하나씩 저장하는 방식으로 다루려면 score1, score2, score3, score4처럼 변수를 네 개 만들어야 합니다. 서른 명이면 서른 개입니다. 게다가 몇 명인지 미리 모르면 아예 만들 수가 없습니다.

리스트(list)

여러 값을 순서대로 저장하고 한 이름으로 다루는 자료형입니다. 리스트에 저장된 값 하나하나를 요소(element)라고 합니다. 이름은 하나지만 요소는 얼마든지 늘어날 수 있습니다.

scores = [90, 85, 72, 68]      # 대괄호로 묶고 쉼표로 구분한다
print(scores)                  # [90, 85, 72, 68]
print(len(scores))             # 4     요소의 개수

번호가 붙은 사물함 한 줄

복도에 사물함이 나란히 붙어 있다고 생각하면 편합니다. 사물함 줄 전체의 이름이 scores이고, 사물함 한 칸이 요소 하나입니다. 칸마다 번호가 붙어 있어 「몇 번 칸」이라고 부르면 그 안의 값을 꺼낼 수 있습니다. 이 번호를 인덱스(index)라고 합니다.

len은 요소가 몇 개인지 세어 반환합니다. 학생이 늘면 요소도 늘어나므로, 몇 명인지 몰라도 코드는 그대로 둘 수 있습니다.

요소가 숫자일 필요는 없습니다. ["가", "나", "다"]처럼 문자열도 되고, 자료형이 섞여도 됩니다. 다만 섞어 두면 꺼내 쓸 때 자료형을 일일이 확인해야 하므로 대개 같은 자료형끼리 모읍니다.

스스로 확인

빈 리스트를 만들려면 어떻게 쓸까요? 그때 len은 무엇을 반환할까요?

인덱스로 꺼내고 바꾸기

첫 요소의 인덱스가 1이 아니라 0인 데는 이유가 있습니다.

scores = [90, 85, 72, 68]

print(scores[0])     # 90    인덱스는 0부터 센다
print(scores[1])     # 85
print(scores[3])     # 68    마지막은 인덱스 3 (개수보다 하나 작다)
print(scores[-1])    # 68    뒤에서 첫 번째
print(scores[-2])    # 72    뒤에서 두 번째

왜 0부터 셀까

인덱스를 「몇 번째인가」가 아니라 「맨 앞에서 얼마나 떨어져 있는가」로 세기 때문입니다. 첫 요소는 맨 앞에서 0만큼 떨어져 있으니 인덱스 0, 그다음 요소가 인덱스 1입니다.

그래서 마지막 인덱스는 개수보다 하나 작습니다. 요소가 네 개면 마지막 인덱스는 3입니다. 이 하나 차이가 처음 배울 때 자주 틀리는 부분입니다.

뒤에서 세는 인덱스

-1은 맨 뒤, -2는 뒤에서 두 번째입니다. 「마지막 값」을 꺼내려고 scores[len(scores) - 1]이라고 쓸 필요가 없습니다. 뒤에서 셀 때만 1부터 시작한다는 점을 눈여겨보세요 — -0은 0과 같아서 맨 앞 요소를 가리키기 때문입니다.

없는 인덱스를 쓰면 그 줄에서 멈춥니다

scores = [90, 85, 72, 68]
print(scores[4])     # IndexError — 인덱스 4는 없다. 마지막은 인덱스 3이다

IndexError는 「그런 인덱스는 없다」는 뜻입니다. 대개 하나 차이로 납니다 — 개수를 마지막 인덱스로 착각한 경우입니다. 오류를 만나면 개수와 마지막 인덱스를 나란히 적어 보세요.

인덱스를 지정해 요소의 값을 바꿀 수도 있습니다.

scores = [90, 85, 72, 68]

scores[2] = 77          # 인덱스를 지정해 값을 바꾼다
print(scores)           # [90, 85, 77, 68]

name = "학생"
# name[0] = "가"        # 오류. 문자열은 이렇게 바꿀 수 없다

문자열은 이렇게 바꿀 수 없습니다. 인덱스로 글자를 꺼내 볼 수는 있지만, 인덱스를 지정해 다른 글자로 바꾸려 하면 오류가 납니다. 리스트는 만든 뒤에도 고칠 수 있고 문자열은 한번 만들면 바꿀 수 없다는 차이입니다.

스스로 확인

값이 다섯 개인 리스트에서 scores[5]는 왜 오류일까요? 마지막 값을 꺼내려면 무엇이라고 써야 할까요?

넣고 빼기

요소의 개수를 미리 정해 두지 않아도 됩니다.

scores = [90, 85]

scores.append(72)          # 맨 뒤에 추가한다
print(scores)              # [90, 85, 72]

scores.insert(1, 100)      # 인덱스 1에 삽입한다
print(scores)              # [90, 100, 85, 72]

scores.remove(85)          # 「값」 85를 찾아 삭제한다
print(scores)              # [90, 100, 72]

last = scores.pop()        # 맨 뒤 요소를 꺼내면서 그 값을 「반환한다」
print(last, scores)        # 72 [90, 100]
하는 일 쓰는 법 메모
맨 뒤에 추가하기 append(값) 가장 많이 쓴다
원하는 위치에 삽입하기 insert(인덱스, 값) 그 뒤 요소들의 인덱스가 하나씩 커진다
값을 찾아 삭제하기 remove(값) 인덱스가 아니라 값을 지정한다. 없으면 오류
꺼내면서 삭제하기 pop() 맨 뒤 요소를 삭제하고 그 값을 반환한다

remove는 「값」, pop은 「인덱스」

remove(2)는 「2라는 값을 찾아 삭제하라」이고 pop(2)는 「인덱스 2의 요소를 꺼내라」입니다. 둘을 바꿔 쓰면 엉뚱한 요소가 삭제되는데도 오류 없이 실행될 때가 많아 알아채기 어렵습니다.

같은 값이 여럿이면 remove는 맨 앞에서 처음 찾은 하나만 삭제합니다.

이 네 가지는 리스트 자체를 고칩니다. 새 리스트를 반환하지 않고 원래 리스트를 바꿉니다. 「통째로 다루기」 절의 sort에서 이 차이가 다시 중요해집니다.

스스로 확인

맨 「앞」에 값을 넣으려면 어떻게 할까요? 그때 원래 있던 요소들의 인덱스는 어떻게 될까요?

일부만 잘라 내기

끝 인덱스가 「포함되지 않는」 것에는 이유가 있습니다.

scores = [90, 85, 72, 68, 55]

print(scores[1:3])     # [85, 72]    인덱스 1부터 3 「앞」까지
print(scores[:2])      # [90, 85]    처음부터
print(scores[3:])      # [68, 55]    끝까지
print(scores[:])       # [90, 85, 72, 68, 55]   통째로

「어디부터 어디 앞까지」로 읽습니다

대괄호 안에 범위를 적어 리스트의 일부를 잘라 내는 것을 슬라이싱(slicing)이라고 합니다. scores[1:3]은 인덱스 1부터 인덱스 3 「앞」까지, 그러니까 인덱스 1과 2입니다. 인덱스 3은 들어가지 않습니다.

이상해 보이지만 개수가 바로 나온다는 장점이 있습니다 — 3 - 1 = 2개. 그리고 [:2]와 [2:]가 겹치지도 빠지지도 않고 딱 나뉩니다.

비워 두면 끝까지

콜론 앞을 비우면 처음부터, 콜론 뒤를 비우면 끝까지입니다. 둘 다 비운 [:]는 처음부터 끝까지 전체를 잘라 내고, 이것으로 「이름을 하나 더 붙이면」 절의 문제를 풉니다.

잘라 낸 결과는 새 리스트입니다. 원래 리스트는 바뀌지 않습니다.

인덱스와 달리 슬라이싱은 범위를 넘겨도 오류가 나지 않습니다. scores[1:100]은 있는 요소까지만 잘라 냅니다. 같은 대괄호를 쓰지만 규칙이 다르니 헷갈리지 않게 주의하세요.

스스로 확인

앞의 두 개를 뺀 나머지를 얻으려면 어떻게 쓸까요? 뒤의 두 개만 얻으려면요?

통째로 다루기

요소를 하나씩 꺼내지 않고 리스트 전체를 한 번에 다루는 방법입니다.

scores = [90, 85, 72, 68]

print(sum(scores))                  # 315
print(max(scores), min(scores))     # 90 68
print(sum(scores) / len(scores))    # 78.75
print(90 in scores)                 # True    들어 있는지 확인한다

sum·max·min·len은 괄호 안에 리스트를 넣으면 요소 전체로 계산한 결과를 반환합니다. 평균을 구하는 함수는 따로 없으므로 합계를 개수로 나눠 구합니다. in은 값이 리스트에 들어 있는지를 참·거짓으로 알려 줍니다.

scores = [90, 85, 72, 68]

print(sorted(scores))              # [68, 72, 85, 90]   「새 리스트」를 만들어 반환한다
print(scores)                      # [90, 85, 72, 68]   원래 리스트는 그대로다

scores.sort()                      # 원래 리스트를 「직접」 정렬한다
print(scores)                      # [68, 72, 85, 90]

scores.sort(reverse=True)
print(scores)                      # [90, 85, 72, 68]

sorted와 sort는 다릅니다

무엇 원래 리스트 반환값
sorted(scores) 그대로 둔다 정렬한 새 리스트
scores.sort() 직접 정렬한다 None(값이 없음)

new = scores.sort()라고 쓰면 new에는 정렬된 리스트가 아니라 None이 저장됩니다. 아주 흔한 실수입니다. None은 파이썬이 「값이 없음」을 나타낼 때 쓰는 특별한 값으로, 반환할 값이 없는 명령은 None을 반환합니다.

원래 순서를 지켜야 하면 sorted, 지킬 필요가 없으면 sort를 씁니다. 큰 값부터 정렬하려면 괄호 안에 reverse=True를 지정합니다.

scores = list(map(int, input("점수를 공백으로 나누어 입력하세요: ").split()))

print("입력한 점수:", scores)
print("개수:", len(scores))
print("합계:", sum(scores))
print("평균:", sum(scores) / len(scores))
print("가장 높은 점수:", max(scores))
print("낮은 순으로:", sorted(scores))

map이 반환한 결과를 list(…)로 감싼 점을 눈여겨보세요. map이 반환한 결과는 len으로 개수를 셀 수 없고, 한 번 끝까지 꺼내 쓰면 비어 버립니다. list로 감싸야 리스트가 되어 len으로 개수를 세고 여러 번 꺼내 쓸 수 있습니다.

스스로 확인

가장 높은 점수를 「뺀」 나머지의 평균을 구하려면 어떤 것들을 이어 쓰면 될까요?

이름을 하나 더 붙이면

복사한 것 같은데 복사가 아닌 경우가 있습니다.

변수에 값을 「저장한다」고 흔히 말하지만, 파이썬에서 대입은 정확히는 값에 이름을 붙이는 일입니다. score = 90은 90이라는 값에 score라는 이름표를 붙인 것이고, 다시 대입하면 이름표를 다른 값으로 옮겨 붙입니다. 숫자 하나를 다룰 때는 두 설명이 같은 결과를 내지만, 리스트에서는 차이가 드러납니다.

a = [1, 2, 3]
b = a                  # 「같은 리스트」에 이름을 하나 더 붙인 것이다
b.append(4)
print(a)               # [1, 2, 3, 4]   a도 함께 바뀐다

c = a[:]               # 통째로 잘라 「새 리스트」를 만든다
c.append(5)
print(a)               # [1, 2, 3, 4]   이번에는 그대로다
print(c)               # [1, 2, 3, 4, 5]

b = a는 사물함을 복제하지 않습니다

같은 사물함 줄에 이름표를 하나 더 붙인 것뿐입니다. b에 값을 추가하면 a로 확인해도 들어가 있습니다. 둘이 같은 것을 가리키고 있으니까요.

a = 3, b = a 뒤에 a = 5라고 하면 b는 3 그대로입니다. a = 5는 이름표 a를 5로 옮겨 붙인 것이지 3을 고친 것이 아니기 때문입니다. b.append(4)는 이름표를 옮겨 붙이지 않고 가리키는 리스트 자체를 고치므로 a로 확인해도 바뀌어 있습니다.

따로 두고 싶으면 잘라서 새로 만듭니다

c = a[:]는 통째로 잘라 새 리스트를 만듭니다. 이제 c를 고쳐도 a는 그대로입니다. 다만 새로 만드는 것은 바깥 리스트 하나뿐입니다. 리스트 안에 또 리스트가 들어 있으면 안쪽 리스트는 두 이름이 함께 가리키므로, 지금은 숫자나 문자열이 저장된 리스트만 이 방법으로 복사합니다. 「일부만 잘라 내기」 절에서 「잘라 낸 결과는 새 리스트」라고 한 것이 여기서 쓰입니다.

이 성질은 불편한 함정이기도 하지만 쓸모도 있습니다. 요소가 수천 개여도 이름을 하나 더 붙이는 데는 시간이 거의 들지 않고, 여러 이름으로 같은 자료를 함께 다룰 수 있습니다. 다만 쓸 때마다 복사인지 이름표인지를 확인합니다.

스스로 확인

b = a 뒤에 b = [1, 2]라고 「새 리스트를 대입하면」 a도 바뀔까요? 앞의 b.append(4)와 무엇이 다를까요?

핵심 정리

인덱스는 0부터

「맨 앞에서 얼마나 떨어졌나」로 센다. 마지막 인덱스는 개수보다 하나 작다. 뒤에서 셀 때는 -1부터. 없는 인덱스를 쓰면 IndexError.

넣고 빼기

append는 맨 뒤, insert는 지정한 위치. remove에는 값, pop에는 인덱스를 지정한다 — 바꿔 쓰면 오류 없이 틀릴 수 있다.

잘라 내기

[1:3]은 인덱스 3 앞까지라 개수가 3-1로 나온다. 비우면 처음부터·끝까지. 결과는 새 리스트이고 범위를 넘겨도 오류가 없다.

sorted와 sort

sorted는 원래 리스트를 그대로 두고 정렬한 새 리스트를 반환한다. sort는 원래 리스트를 직접 정렬하고 None을 반환한다.

이름표와 복사

b = a는 같은 리스트에 이름을 하나 더 붙인 것이라 함께 바뀐다. 따로 두려면 c = a[:]처럼 잘라서 새로 만든다.

확인 퀴즈

배운 내용을 확인해 봅시다.

1. scores = [90, 85, 72, 68]에서 마지막 값을 꺼내는 방법으로 틀린 것은?

2. new = scores.sort()를 실행하면 new에는 무엇이 들어 있나?

3. a = [1, 2, 3], b = a, b.append(4) 다음 a는?