프로그래밍

반복 구조

서른 명의 점수를 서른 줄로 적을 수는 없습니다.
한 줄만 적고 여러 번 실행되게 합니다.

학습 목표

  1. 리스트의 요소를 하나씩 꺼내 같은 작업을 반복할 수 있다.
  2. 정해진 횟수만큼 반복하는 방법과 조건이 참인 동안 반복하는 방법을 구별해 고를 수 있다.
  3. 값을 누적하는 변수를 알맞은 위치에서 초기화하고, 조건에 맞는 값만 골라낼 수 있다.
  4. 반복을 끝내거나 이번 반복만 건너뛸 수 있고, 반복을 중첩했을 때 실행 순서를 따라갈 수 있다.

같은 일을 여러 번

리스트에 저장한 값을 하나씩 꺼내 보려는 순간 막힙니다.

리스트에 저장된 점수를 하나씩 출력하려면 인덱스로 하나씩 꺼내 이렇게 써야 합니다.

scores = [90, 85, 72, 68]

print(scores[0])
print(scores[1])
print(scores[2])
print(scores[3])
# 서른 명이면 서른 줄. 게다가 몇 명인지 미리 모르면 아예 적을 수 없다

네 개는 견딜 만합니다. 그런데 서른 명이면 서른 줄이고, 무엇보다 몇 명인지 미리 모르면 아예 적을 수 없습니다. 리스트는 값이 몇 개든 저장할 수 있는데, 꺼내는 코드가 개수에 묶여 있으면 그 장점을 절반밖에 살리지 못합니다.

반복 구조

같은 줄을 여러 번 적는 대신, 한 번만 적고 여러 번 실행되게 하는 방식입니다. 코드의 줄 수가 요소의 개수와 상관없어지므로 네 명이든 서른 명이든 코드가 같습니다.

트랙을 도는 것으로 생각합니다

달리는 사람은 같은 트랙을 계속 돕니다. 코드도 마찬가지로 들여쓴 같은 줄들을 반복해서 실행합니다. 트랙 한 바퀴가 「반복 한 번」에 해당합니다.

스스로 확인

반복 없이 리스트의 값을 하나도 빠뜨리지 않고 꺼내려면, 코드를 쓰는 사람이 무엇을 세고 있어야 할까요?

하나씩 꺼내 주기

파이썬의 반복은 「몇 번」이 아니라 「무엇을 하나씩」에서 출발합니다.

scores = [90, 85, 72, 68]

for score in scores:          # 하나씩 꺼내 score에 대입한다
    print(score)              # 90 / 85 / 72 / 68  네 줄

읽는 법

「scores에서 값을 하나씩 꺼내 score에 대입하고, 그때마다 들여쓴 줄들을 실행하라」입니다. 소리 내어 읽으면 「scores 안의 score마다」가 되어 뜻이 그대로 들립니다.

  • 몇 개인지 셀 필요가 없다 — 리스트가 끝나면 저절로 멈춘다.
  • 인덱스를 쓰지 않는다 — IndexError가 날 일이 아예 없다.
  • 꺼낸 값이 대입되는 변수(score)의 이름은 내가 정한다.

꺼낼 리스트가 없고 횟수만 필요할 때도 있습니다. 그때는 range로 수를 차례로 만들어 하나씩 꺼냅니다.

for i in range(5):            # 0, 1, 2, 3, 4   다섯 번
    print(i, end=" ")
print()

for i in range(1, 6):         # 1부터 6 「앞」까지
    print(i, end=" ")
print()

for i in range(0, 10, 3):     # 0부터 10 앞까지 3씩
    print(i, end=" ")
print()
쓰는 법 나오는 수 메모
range(5) 0 1 2 3 4 0부터 시작해 다섯 개
range(1, 6) 1 2 3 4 5 6은 들어가지 않는다
range(0, 10, 3) 0 3 6 9 세 번째 수가 간격(step)

끝이 빠지는 규칙은 슬라이싱과 같습니다

range(1, 6)에 6이 안 들어가는 것은 슬라이싱 [1:6]이 인덱스 6 앞까지인 것과 같은 규칙입니다. 덕분에 range(5)가 정확히 다섯 번이 되고, 인덱스와도 딱 맞습니다 — 요소가 다섯 개면 인덱스가 0~4이니까요.

리스트의 요소를 하나씩 차례로 꺼내 처리하는 것을 순회(traversal)라고 합니다. 리스트를 순회할 때는 range를 쓰지 않는 편이 낫습니다. for i in range(len(scores))처럼 쓰면 인덱스로 값을 다시 꺼내야 하고 하나 차이로 어긋날 위험이 생깁니다. 인덱스가 꼭 필요할 때만 range를 씁니다.

스스로 확인

range(2, 11, 2)는 어떤 수들을 만들까요? 몇 개일까요?

누적하고 골라내기

반복은 값을 누적하거나 골라낼 때 특히 쓸모가 많습니다.

scores = [90, 85, 72, 68]
total = 0                     # 누적할 변수를 「반복 밖」에서 초기화한다

for score in scores:
    total = total + score     # 앞선 반복의 결과를 이어받는다

print(total)                  # 315

total = total + score는 대입 규칙 그대로입니다 — 오른쪽을 먼저 계산해 왼쪽 변수에 대입합니다. 반복할 때마다 「지금까지의 합에 이번 값을 더해 다시 대입하는」 일이 일어납니다. 이렇게 값을 계속 더해 나가는 것을 누적(accumulation)이라고 합니다. 반복 전에 total = 0처럼 변수에 처음 값을 대입해 두는 것은 초기화(initialization)라고 합니다.

누적할 변수를 「안」에서 초기화하면 합이 쌓이지 않습니다

scores = [90, 85, 72, 68]

for score in scores:
    total = 0                 # 반복 「안」에서 초기화하면 반복할 때마다 0으로 되돌아간다
    total = total + score
    print(total, end=" ")     # 90 85 72 68   합이 누적되지 않는다
print()

반복할 때마다 total이 0으로 되돌아가므로 앞에서 더한 값이 사라집니다. 누적할 변수는 반복 「밖」에서 「한 번만」 초기화합니다. 반대로 반복할 때마다 새로 시작해야 하는 값은 안에서 초기화하는 것이 맞습니다 — 안이 늘 틀린 것이 아니라 무엇을 하려는지에 달렸습니다.

누적하는 것이 숫자일 필요는 없습니다. 빈 리스트에서 시작해 조건에 맞는 값만 추가하면 원하는 값을 골라낼 수 있습니다. 반복 안에 if를 둔 구조로, 실제 프로그램에서 매우 자주 쓰입니다.

scores = [90, 55, 72, 48, 85]
passed = []                   # 빈 리스트에서 시작한다

for score in scores:
    if score >= 60:           # 반복 「안」에서 조건을 검사한다
        passed.append(score)

print(passed)                 # [90, 72, 85]
print(len(passed), "명이 합격했습니다")   # 3 명이 합격했습니다

앞에서 배운 세 가지가 함께 쓰였습니다

  • 리스트가 값을 저장하고, 빈 리스트에 결과를 모은다.
  • 반복이 값을 하나씩 꺼낸다.
  • 선택 구조가 추가할지 말지 정한다.

합계처럼 자주 쓰는 것은 이미 만들어져 있습니다 — sum(scores) 한 줄이면 됩니다. 그래도 반복으로 직접 써 보는 것은 안에서 무슨 일이 일어나는지 알기 위해서입니다. 「조건에 맞는 것만 더하기」처럼 조금만 달라져도 직접 써야 합니다.

스스로 확인

곱을 누적할 변수는 0으로 초기화하면 안 됩니다. 왜일까요? 무엇으로 초기화해야 할까요?

조건으로 반복하기

몇 번 반복할지 「미리 모를」 때 쓰는 방식입니다.

지금까지는 반복 횟수가 정해져 있었습니다 — 리스트의 요소 개수만큼, 또는 range가 만든 수만큼. 그런데 언제 끝날지 미리 알 수 없는 일도 있습니다. 사용자가 그만두겠다고 할 때까지 입력받는 경우가 그렇습니다.

count = 3

while count > 0:              # 조건이 참인 「동안」 반복한다
    print(count)              # 3 / 2 / 1
    count = count - 1         # 이것을 빠뜨리면 영원히 반복한다

print("발사")

반복에 필요한 세 가지

while은 「조건이 참인 동안 계속」이라는 뜻입니다. 반복이 한 번 끝날 때마다 조건을 다시 검사합니다. 그래서 ①시작 ②조건 ③변화 셋을 내가 챙겨야 합니다 — for에서는 이 셋을 파이썬이 알아서 해 주고 있었던 것입니다.

변화를 빠뜨리면 반복이 끝나지 않습니다

count = 3

while count > 0:
    print(count)
    # 줄이는 줄을 빠뜨렸다 -> count가 영원히 3이라 조건이 영원히 참이다

# 이 코드는 멈추지 않는다. 실행하면 Ctrl+C를 눌러 멈춰야 한다

count가 영원히 3이므로 조건도 영원히 참입니다. 출발선에 선 채로 「아직 도착하지 않았다」를 끝없이 확인하는 것과 같습니다. 화면에 3이 끝없이 출력되고 Ctrl+C를 눌러야 멈춥니다. 이렇게 끝나지 않는 반복을 무한 루프(infinite loop)라고 합니다.

처음 배울 때 거의 모두 겪습니다. 고장이 아니라 코드에 쓴 대로 실행된 것이니 당황하지 말고 ③ 변화가 있는지부터 확인하세요.

둘 중 무엇을 쓸까

리스트를 순회하거나 횟수가 정해져 있으면 for, 언제 끝날지 미리 모르면 while입니다. 파이썬에서는 for로 되는 일이 훨씬 많으므로, while을 쓰기 전에 「반복 횟수를 정말 미리 알 수 없는가」를 한 번 검토해 보세요.

스스로 확인

1부터 시작해 더해 나가다가 합이 처음으로 100을 넘는 순간을 찾으려면 둘 중 무엇이 어울릴까요?

끝내기와 건너뛰기

반복을 아예 끝내는 것과 이번 반복만 건너뛰는 것은 다릅니다.

break — 반복을 끝낸다

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

for score in scores:
    if score < 60:
        print("낙제 점수를 찾았습니다:", score)   # 55에서 멈춘다
        break                                    # 반복 「전체」를 끝낸다
    print("확인:", score)

반복 전체를 끝냅니다. 남은 요소가 있어도 더 꺼내지 않습니다. 「찾으면 그만」인 일에 씁니다.

continue — 이번 반복만 건너뛴다

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

for score in scores:
    if score < 60:
        continue                 # 이번 반복만 건너뛰고 「다음 반복으로」 넘어간다
    print("합격:", score)        # 90 / 72 / 85

이번 반복의 남은 줄만 건너뜁니다. 반복은 끝나지 않고 다음 값으로 이어집니다.

끝을 반복 「안」에서 정하는 구조

total = 0

while True:                       # 「참」이므로 스스로는 끝나지 않는다
    line = input("점수 (끝내려면 빈 줄): ")
    if line == "":
        break                     # 반복을 끝내는 곳을 「안」에 둔다
    total = total + int(line)

print("합계", total)

while True는 조건이 늘 참이라 스스로는 끝나지 않는 반복입니다. 대신 반복을 끝내는 break를 반복 안에 둡니다. 「입력받아 보고 나서야 끝인지 알 수 있는」 일에 어울립니다 — 입력받기 전에는 조건식에 쓸 값이 없기 때문입니다.

break가 없으면 그대로 무한 루프가 되므로, while True를 쓸 때는 break가 실행되는 경우가 있는지 반드시 확인합니다.

스스로 확인

리스트에서 60점 미만이 하나라도 있는지만 알고 싶다면 break를 어떻게 쓸까요?

반복 안의 반복

시계의 분침과 초침을 떠올리면 그림이 그려집니다.

시계에서 분침이 한 칸 갈 동안 초침은 예순 칸을 움직입니다. 초침이 한 바퀴를 다 돌아야 분침이 비로소 한 칸 움직입니다. 바깥 반복이 분침, 안쪽 반복이 초침입니다.

for dan in range(2, 5):
    print("---", dan, "단 ---")
    for i in range(1, 4):        # 바깥 반복 한 번마다 안쪽 반복이 처음부터 끝까지 실행된다
        print(dan, "x", i, "=", dan * i)

바깥 반복이 3번, 안쪽 반복이 3번이면 안쪽 줄들은 3 × 3 = 9번 실행됩니다. 단 제목(--- 2 단 ---)을 출력하는 줄이 바깥 반복 안, 안쪽 반복 앞에 있어서 단이 바뀔 때마다 한 번씩 출력됩니다.

중첩할수록 들여쓰기를 정확히 맞춥니다

파이썬은 들여쓰기로 줄이 어느 반복에 속하는지 정하므로, 중첩할 때 한 단계(공백 네 개)만 덜 들여써도 안쪽에 있어야 할 줄이 바깥으로 나갑니다. 공백 한두 개만 어긋나면 대개 IndentationError가 나서 곧 알 수 있지만, 한 단계가 통째로 어긋나면 오류가 나지 않고 결과만 달라져 찾기 어렵습니다. 한 단계 들어갈 때마다 같은 개수(관례로 공백 네 개)씩 정확히 들여쓰세요.

안쪽 반복에서 break를 쓰면 안쪽 반복만 끝납니다. 바깥 반복은 그대로 이어집니다. 중첩이 두 단계를 넘으면 대개 읽기 어려워집니다.

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

total = 0
passed = []

for score in scores:
    total = total + score
    if score >= 60:
        passed.append(score)

print("합계", total, "평균", total / len(scores))
print("합격", len(passed), "명:", passed)

스스로 확인

구구단을 2단부터 9단까지, 각 단을 한 줄에 이어서 출력하려면 어디에 무엇을 두어야 할까요?

핵심 정리

for는 하나씩 꺼내 준다

개수를 셀 필요도, 인덱스를 쓸 필요도 없다. 횟수만 필요하면 range로 수를 만들어 하나씩 꺼낸다.

range의 끝

range(1, 6)에 6은 들어가지 않는다. 슬라이싱과 같은 규칙이라 range(5)가 정확히 다섯 번이 된다.

누적할 변수

누적할 변수는 반복 밖에서 한 번만 초기화한다. 빈 리스트에서 시작해 조건에 맞는 값만 추가하면 원하는 값을 골라낼 수 있다.

while과 무한 루프

언제 끝날지 모를 때 쓴다. ③ 변화를 빠뜨리면 멈추지 않는다 — 멈추지 않으면 ③부터 확인한다.

break와 continue, 그리고 중첩

break는 반복 전체를 끝내고, continue는 이번 반복의 남은 줄만 건너뛴다. while True는 끝을 반복 안에서 break로 정한다. 중첩하면 안쪽 break는 안쪽 반복만 벗어난다.

확인 퀴즈

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

1. for i in range(1, 6):는 몇 번 반복하는가?

2. total = 0을 반복 안으로 옮기면?

3. 다음 중 틀린 설명은?