이 절에서는 if 문, while 문, for 문을 사용하지 않고 선택과 반복을 수행하는 방법을 알아 본다. 알아두면 다른 사람의 코드를 읽을 때 도움이 되지만 지금 수준에서 반드시 알아야 하는 것은 아니니, 어렵게 느껴진다면 건너뛰어도 괜찮다.

6.3.1 단축 평가

단축 평가(short-circuit evaluation)란 계산을 진행하는 도중에 결과가 이미 확정된 경우, 나머지 계산 과정을 생략하는 것이다. and 연산과 or 연산은 연산의 특성상 단축 평가를 할 수 있다. 컴퓨터의 계산 자원을 절약하기 위해 파이썬을 포함한 대부분의 프로그래밍 언어는 이 두 연산에 단축 평가를 적용하고 있다.

and 연산은 좌변과 우변이 모두 참이어야 참으로 평가된다. 만약 좌변이 거짓이라면, 우변은 살펴볼 볼 필요도 없이 거짓이다.

코드 6-39 and 연산의 단축 평가

(1 == 2) and (3 < 4) and (True != False)

and 좌변의 식 (1 == 2)는 거짓이므로, 우변의 식 (3 < 4) and (True != False)는 평가해 보지 않아도 거짓임을 판단할 수 있다. 컴퓨터는 (1 == 2) 식만을 평가한 후 거짓임을 알고 나머지는 계산하지 않은 채 거짓으로 결론내린다.

이와 유사하게, or 연산은 좌변과 우변이 모두 거짓이어야 거짓으로 평가된다. 만일 좌변이 참이라면, 우변의 계산을 생략할 수 있다.

코드 6-40 or 연산의 단축 평가

('red' in ['red', 'violet', 'blue']) or (1 == 10 - 9) and (20 < 20)

or 좌변의 식 ('red' in ['red', 'violet', 'blue'])은 참이다. 컴퓨터는 우변의 식 (1 == 10 - 9) and (20 < 20)을 계산하지 않고 전체 식의 결과를 참으로 결론내린다.

단축 평가를 이용한 선택

and 연산과 or 연산의 단축 평가 특성을 이용하면 if 문을 사용하지 않고도 선택을 나타낼 수 있다.

코드 6-41 and 와 or 를 이용한 선택

def 홀짝(n):
    """n이 홀수인지 짝수인지를 화면에 출력한다."""
    (n % 2 == 0) and print(n, '은 짝수입니다.')
    (n % 2 == 0) or print(n, '은 홀수입니다.')

홀짝(10)
홀짝(11)

실행 결과:

10 은 짝수입니다.
11 은 홀수입니다.

코드 6-41의 홀짝()함수에서 n이 짝수일 때 and 연산과 or 연산의 좌변은 참이 된다. and 연산에서는 우변도 참인지 확인하기 위해 코드가 실행되지만, or 연산에서는 우변이 참인지 확인할 필요가 없어 코드가 실행되지 않는다. n이 홀수일 때는 그 반대다.

단축 평가를 if 문 대신 사용하면 코드가 간결해지지만 이런 표현에 익숙하지 않은 프로그래머들을 헷갈리게 만들 수 있다. 따라서 단축 평가보다는 if 문을 사용하는 것이 좋다. 남의 코드를 읽기 위해서만 알아두도록 하자.

6.3.2 재귀

내가 내 이름을 부를 수 있는 것처럼, 함수도 자기 자신을 호출할 수 있다. 함수가 자신을 직접 또는 간접적으로 호출하는 것을 재귀(recursion)라고 한다. 재귀를 사용하면 while 문이나 for 문 없이도 반복 작업을 수행할 수 있다.

코드 6-42 n 이상 m 미만의 자연수 출력

def 자연수(n, m):
    """수 n을 출력하고, 1 더한 수가 m보다 작으면 그 수도 출력한다."""
    print(n)
    if n + 1 < m:
        자연수(n + 1, m)

자연수(4, 8)

실행 결과:

4
5
6
7

코드 6-42의 자연수() 함수는 자연수 n과 m을 입력받아, 그 사이의 모든 자연수를 출력하는 함수다. 이 함수는 본문 안에서 자기자신(자연수())을 호출한다. 이 함수가 실행되는 과정을 생각해 보자.

  1. 처음에 자연수(4, 8)을 호출. 화면에 4가 출력된다. 4 + 1 < 8는 참이므로, 자연수(4 + 1, 8)이 재귀 호출된다.
  2. 다시 호출된 자연수()함수는 전달받은 수 n을 출력하고, 조건을 검사해 자연수(n + 1, m)을 다시 호출한다.
  3. 자연수() 함수는 계속 동일한 형태로 호출되지만, 이 함수의 매개변수 n에 전달되는 값은 계속 변한다. n + 1 < m 이라는 조건이 거짓이 된 호출에서 재귀 호출은 더이상 일어나지 않는다.

이처럼 재귀 함수는 while 문이나 for 문과 형태는 다르지만 그들과 마찬가지로 반복의 시작, 각 주기의 변화, 반복의 유지 조건(종료 조건)이 있다. 재귀는 처음에 어렵게 느껴질 수 있지만, 언제 자기 자신을 호출할 것인지, 어떤 값을 변화시켜 매개변수로 전달할 것인지를 잘 신경쓰면 쉽게 작성할 수 있다.

재귀 활용하기

피보나치 수열은 1, 1, 2, 3, 5, 8, … 순으로 앞의 두 수를 합한 수를 나열한 수열이다. 예를 들어 다섯 번째 피보나치 수는 세 번째 수 2와 네 번째 수 3의 합이다. 다음 세 가지 규칙만 알면 몇 번째 피보나치 수라도 계산할 수 있다.

  • 규칙 1: 첫 번째 피보나치 수는 1
  • 규칙 2: 두 번째 피보나치 수는 1
  • 규칙 3: 그 후, N 번째 피보나치 수는 (N - 1) 번째 피보나치 수와 (N - 2) 번째 피보나치 수의 합

규칙 3에서 피보나치 수를 구하는 방법 자신이 피보나치 수를 구하는 방법을 사용하고 있다. 이 방법처럼 문제 해결 방법이 그 자신을 이용하는 경우가 있다. 재귀를 활용하면 이런 문제 해결법을 그대로 프로그램으로 옮길 수 있다. 다음 코드는 피보나치 수를 구하는 규칙을 그대로 재귀 함수로 옮긴 것이다. 파이참에서 fibonacci.py 파일을 만들고 다음 코드를 입력하여 실행해 보자.

코드 6-43 재귀를 이용한 피보나치 수 계산 (fibonacci.py)

def n번째_피보나치_수(n):
    """n번째 피보나치 수를 반환한다."""
    if n == 1:     # 첫 번째 피보나치 수는 1
        return 1
    elif n == 2:   # 두 번째 피보나치 수는 1
        return 1
    else:          # 그 후의 피보나치 수
        return n번째_피보나치_수(n - 1) + n번째_피보나치_수(n - 2)

# 1번째부터 11번째 피보나치 수까지 출력
for n range(1, 12):
    print(n번째_피보나치_수(n), end=' ')

실행 결과:

1
1
2
3
5
8
13
21
34
55
89

재귀의 단점

재귀는 특정한 문제를 해결할 때 편리하지만 단점도 있다. 재귀는 함수의 실행이 끝나기도 전에 또 다시 함수를 실행하기 때문에, 재귀 호출이 연달아 많이 일어나면 컴퓨터의 계산 자원과 기억 자원을 빠르게 소모할 수 있다. 코드 6-42에서 정의한 자연수() 함수를 이용해 1에서 1천까지의 자연수를 출력하도록 해 보면 다음과 같이 오류가 발생한다.

코드 6-44 재귀 호출이 여러 번 누적되면 오류가 발생한다

>>> 자연수(1, 1000)
1
2
(중략)
995
996
RecursionError: maximum recursion depth exceeded while calling a Python object

마지막의 오류 메시지를 해석하면 “재귀 오류: 파이썬 객체를 호출하는 도중 최대 재귀 깊이를 초과했습니다.”로, 파이썬에서 제한해 놓은 최대 재귀 호출 단계를 넘어서서 오류가 발생했다는 뜻이다. 재귀를 무한정 허용하면 컴퓨터의 자원을 다 써버릴 수 있기 때문에 파이썬에서는 재귀의 호출을 일정 수준에서 제한해 두었다.

재귀를 활용하는 것을 권장하는 프로그래밍 언어도 있지만, 파이썬에서는 while 문과 for 문을 활용하여 반복을 수행하는 것을 권장한다. 파이썬에서는 재귀를 꼭 필요할 때만 사용해야 하며 최대 호출 단계를 넘지 않도록 주의해야 한다. 재귀로 작성한 코드는 동일한 작업을 수행하는 while 문으로 수정할 수 있다.