본문 바로가기

변수·조건·반복으로 상태 읽기

대입이 값을 바꾸는 순서와 if·for·while이 실행할 줄을 고르는 방식을, 변수의 값을 표로 적어 가며 따라갑니다.

알고리즘을 읽는다는 것은 결국 "지금 이 순간 각 변수에 어떤 값이 들어 있는가"를 따라가는 일입니다. 정렬이든 탐색이든, 알고리즘은 값을 조금씩 바꾸다가 원하는 상태에 도착하면 멈추는 순서이기 때문입니다. 이 글에서는 그 순서를 만드는 세 가지 도구인 대입, 조건, 반복을 다룹니다. 코드를 읽을 때마다 변수의 값을 표로 적어 보는 습관도 함께 들입니다. 앞 글에서 파일을 저장하고 실행하는 방법을 익혔다면 바로 시작할 수 있습니다.

대입은 오른쪽을 먼저 계산한다

변수(variable)는 값에 붙인 이름입니다. count = count + 1이라는 줄은 수학의 등식처럼 보이지만 "양쪽이 같다"는 뜻이 아닙니다. Python은 먼저 오른쪽 count + 1을 지금의 count 값으로 계산하고, 그 결과를 다시 count라는 이름에 붙입니다. 이렇게 이름에 새 값을 연결하는 일을 대입(assignment)이라고 합니다.

count = 0
count = count + 1  # 오른쪽: 0 + 1, count=1
count = count + 1  # 오른쪽: 1 + 1, count=2
count += 1         # count = count + 1과 같다, count=3
print(count)
3

count += 1은 count = count + 1을 줄여 쓴 것입니다. 대입은 = 하나로 쓰고, 두 값이 같은지 비교할 때는 ==를 씁니다. count == 3은 count를 바꾸지 않고 참(True)인지 거짓(False)인지만 알려 줍니다. 둘을 헷갈리면 비교하려던 자리에서 값을 바꾸거나 그 반대가 되므로, 처음에는 줄마다 어느 쪽인지 소리 내어 읽어 보는 것도 방법입니다.

조건에 따라 실행할 줄이 달라진다

if 문은 조건이 참일 때만 들여쓴 줄을 실행합니다. 조건이 거짓이면 들여쓴 줄을 건너뛰고, else가 있으면 그쪽을 대신 실행합니다.

n = 7
if n % 2 == 1:
    print(n, "홀수")
else:
    print(n, "짝수")
print("판정 끝")
7 홀수
판정 끝

%는 나머지를 구하는 연산자로, 7 % 2는 7을 2로 나눈 나머지 1입니다. 양의 정수에서는 2로 나눈 나머지가 1이면 홀수, 0이면 짝수입니다. 마지막 줄의 print("판정 끝")은 들여쓰지 않았으므로 if나 else에 속하지 않고, 조건과 상관없이 항상 실행됩니다.

Python은 이렇게 들여쓰기로 문장의 묶음(블록)을 정합니다. 다른 언어에서 중괄호가 하는 일을 들여쓰기가 대신하는 셈이라, 같은 블록의 줄은 모두 같은 칸만큼 들여써야 합니다. 이 교재는 공백 네 칸을 씁니다.

for는 원소를 하나씩 꺼낸다

for 문은 여러 값을 앞에서부터 하나씩 꺼내며 같은 코드를 되풀이합니다. 정해진 횟수만큼 반복할 때는 정수의 범위를 만드는 range와 함께 씁니다.

for n in range(5):
    print(n)
0
1
2
3
4

range(5)는 0부터 시작해 5 바로 앞인 4에서 끝납니다. 시작과 끝을 함께 주는 range(1, 6)은 1부터 5까지입니다. 이처럼 시작은 포함하고 끝은 포함하지 않는 범위를 반열린 구간(half-open interval)라고 합니다. 처음에는 끝값이 빠지는 것이 어색하지만, 이 약속 덕분에 계산이 단순해집니다. range(a, b)에 들어 있는 정수의 개수가 언제나 b - a이고, range(0, n)은 정확히 n번 반복합니다.

더 깊이: 끝을 빼는 범위가 편한 이유

반열린 구간은 범위를 이어 붙이거나 나눌 때 특히 편합니다. range(0, 3)과 range(3, 6)을 이으면 겹치는 값도 빠지는 값도 없이 range(0, 6)이 됩니다. 앞 범위의 끝과 뒤 범위의 시작이 같은 숫자 3이기 때문입니다. 끝을 포함하는 방식이라면 "0부터 2까지"와 "3부터 5까지"처럼 한쪽에 1을 더하거나 빼야 하고, 그 과정에서 하나를 빠뜨리거나 두 번 세는 실수가 자주 생깁니다. 뒤에서 목록을 반으로 나누는 알고리즘을 다룰 때 이 성질을 계속 이용합니다.

표에 적으며 반복 따라가기

이제 대입, 조건, 반복을 한데 모아 1부터 5까지의 홀수를 더하고 개수를 세어 보겠습니다. 코드를 실행하기 전에 반복이 한 번 돌 때마다 변수가 어떻게 바뀌는지 표로 먼저 예측합니다.

total = 0
count = 0
for n in range(1, 6):
    if n % 2 == 1:
        total += n
        count += 1
print(total, count)
꺼낸 nn % 2 == 1실행 뒤 total실행 뒤 count
1참11
2거짓11
3참42
4거짓42
5참93

조건이 거짓인 줄에서는 들여쓴 두 줄을 건너뛰므로 total과 count가 그대로입니다. 표의 마지막 줄이 맞다면 출력은 9 3이어야 합니다. 실제로 실행해 예측과 비교해 보세요.

9 3

표의 각 줄을 보면, 반복이 끝날 때마다 참으로 남아 있는 사실이 하나 있습니다. total은 지금까지 꺼낸 수 가운데 홀수의 합이고, count는 그 홀수의 개수라는 것입니다. 이 사실은 반복을 시작하기 전(둘 다 0)에도 참이고, 한 번 돌 때마다 새로 꺼낸 n까지 포함한 상태로 다시 참이 됩니다. 그래서 마지막 원소까지 처리하고 나면 total과 count는 범위 전체의 홀수 합과 개수가 됩니다. 이렇게 반복마다 유지되는 성질을 불변조건(invariant)이라고 부르는데, 2장에서 알고리즘이 맞는지 따질 때 중요한 도구로 다시 만납니다.

while은 조건이 참인 동안 되풀이한다

for가 정해진 값들을 차례로 꺼낸다면, while은 조건이 참인 동안 몇 번이고 되풀이합니다. 그래서 몇 번 반복할지 미리 알 수 없을 때 씁니다.

remaining = 3
while remaining > 0:
    print(remaining)
    remaining -= 1
print("발사")
3
2
1
발사

while은 반복을 시작할 때마다 조건 remaining > 0을 다시 검사합니다. remaining이 3, 2, 1일 때는 참이라 본문을 실행하고, 1을 뺀 뒤 0이 되면 조건이 거짓이 되어 반복을 빠져나갑니다.

이 반복이 반드시 끝나는 이유는 두 가지입니다. 매번 remaining이 정확히 1씩 줄어들고, 0 이하가 되면 조건이 거짓이 됩니다. 시작값이 얼마든 줄어드는 정수는 언젠가 0에 닿으므로 반복은 끝납니다. 반대로 remaining -= 1을 빠뜨리면 조건이 영원히 참으로 남아 프로그램이 멈추지 않습니다. while을 쓸 때마다 "반복할 때마다 조건 쪽으로 다가가는 값이 있는가"를 확인하세요.

반복 횟수와 작업량

홀수를 세는 반복은 범위의 끝이 늘어나는 만큼 더 돕니다. range(1, n + 1)로 바꾸면 n번 반복하고, 매번 나머지 계산과 비교를 한 번씩 합니다. 그래서 n이 두 배가 되면 할 일도 대략 두 배가 됩니다. 이 교재는 나머지 계산, 비교, 작은 정수의 덧셈 같은 연산을 각각 한 단계로 세는 단순한 방법으로 작업량을 가늠합니다. 한편 변수는 total, count, n 세 개뿐이라 n이 아무리 커져도 필요한 메모리는 늘지 않습니다.

정리

  • x = 식은 오른쪽을 지금의 값으로 먼저 계산한 뒤 그 결과를 x에 연결합니다. 대입은 =, 비교는 ==입니다.
  • if는 조건이 참일 때만 들여쓴 블록을 실행하고, Python은 들여쓰기로 블록을 구분합니다.
  • for는 값을 하나씩 꺼내 반복합니다. range(a, b)는 a부터 b 바로 앞까지이며 개수는 b - a입니다.
  • while은 조건이 참인 동안 반복합니다. 반복마다 조건 쪽으로 다가가는 값이 있어야 끝납니다.
  • 변수의 값을 표로 적으며 따라가면, 반복마다 유지되는 성질(불변조건)이 보입니다.

연습 문제

1. 범위를 늘리면

홀수의 합과 개수를 세는 코드에서 range(1, 6)을 range(1, 8)로 바꾸면 무엇이 출력될까요? 표에 새로 추가될 줄을 먼저 적어 보세요.

해설 보기

새로 꺼내는 값은 6과 7입니다. 6은 짝수라 상태가 그대로이고, 7은 홀수라 total이 9에서 16으로, count가 3에서 4로 바뀝니다.

꺼낸 nn % 2 == 1실행 뒤 total실행 뒤 count
6거짓93
7참164
total = 0
count = 0
for n in range(1, 8):
    if n % 2 == 1:
        total += n
        count += 1
print(total, count)
16 4

range(1, 8)은 8을 포함하지 않으므로 7까지만 꺼낸다는 점을 다시 확인할 수 있습니다.

2. 줄어들지 않는 반복

카운트다운 코드에서 remaining -= 1을 지우면 어떻게 될까요? 실행하기 전에 반복이 끝나는지부터 판단해 보세요.

해설 보기

remaining이 3에서 바뀌지 않으므로 조건 remaining > 0이 계속 참입니다. 반복이 끝나지 않고 3이 끝없이 출력되며, 마지막 줄의 발사에는 영원히 도달하지 못합니다. 이런 상태를 무한 루프(infinite loop)라고 합니다. 실수로 실행했다면 터미널에서 Ctrl+C를 눌러 프로그램을 중단할 수 있습니다.

3. 한 번도 돌지 않는 반복

다음 코드는 무엇을 출력할까요? 두 반복의 본문이 각각 몇 번 실행되는지 세어 보세요.

for n in range(0):
    print("for 안", n)

remaining = 0
while remaining > 0:
    print("while 안", remaining)
    remaining -= 1

print("끝")
해설 보기

range(0)은 0부터 0 바로 앞까지라 꺼낼 값이 하나도 없고, while은 처음부터 조건이 거짓입니다. 두 반복 모두 본문을 한 번도 실행하지 않으므로 끝만 출력됩니다.

끝

반복이 한 번도 돌지 않는 경우는 오류가 아니라 정상적인 상황입니다. 빈 목록을 처리하거나 할 일이 처음부터 없는 경우가 이에 해당하며, 알고리즘을 검사할 때 꼭 넣어 보아야 할 입력이기도 합니다.

다음 글 함수·리스트·복사와 작은 검사에서는 이 반복들을 함수로 묶어 여러 입력에 다시 쓰고, 값 여러 개를 담는 리스트를 다룹니다.