조건문과 재귀

나머지 연산자

나머지 연산자는 정수에 작용해서 첫 번째 피 연산자를 두 번째로 나눌 경우의 나머지를 줍니다. 파이썬에서 나머지 연산자는 백분율 기호 (%) 입니다.. 문법은 다른 연산자들과 마찬가지입니다:

>>> quotient = 7 / 3
>>> print quotient
2
>>> remainder = 7 % 3
>>> print remainder
1

그래서 7을 3으로 나누면 몫이 2이고 1이 남습니다.

나머지 연산자는 놀라울 정도로 쓸모 있습니다. 예를 들어, 한 숫자가 다른 숫자로 나누어 떨어지는지 검사할 수 있습니다—만약 x % y 가 영이면, x 는 y 로 나누어 떨어집니다.

또, 가장 오른쪽 끝의 숫자들을 추출할 수 있습니다. 예를 들어, x % 10 는 x 의 (10진법으로) 가장 오른쪽 숫자를 줍니다. 마찬가지로, x % 100 는 마지막 두 자리 숫자를 줍니다.

논리식

논리식은 참 또는 거짓 값을 갖는 표현식입니다. 다음 예는 == 연산자를 사용하는데, 두 피 연산자를 비교한 후 같으면 True를, 그렇지 않으면 False를 만듭니다:

>>> 5 == 5
True
>>> 5 == 6
False

True 와 False 는 bool 형에 속하는 특별한 값입니다; 문자열이 아닙니다:

>>> type(True)
<type 'bool'>
>>> type(False)
<type 'bool'>

== 연산자는 비교 연산자들 중 하나인데, 다른 것들은 이렇습니다:

x != y               # x 는 y 와 같지 않다
x > y                # x 는 y 보다 크다
x < y                # x 는 y 보자 작다
x >= y               # x 는 y 보다 크거나 같다
x <= y               # x 는 y 보다 작거나 같다

이 연산자들이 아마도 익숙하게 느껴지겠지만, 파이썬 기호는 수학 기호와는 좀 다릅니다. 일반 적인 오류는 이중등호 (==) 대신에 하나의 등호 (=) 를 사용하는 것입니다. = 는 대입 연산자고, == 는 비교 연산자임을 기억하세요. =< 나 => 같은 것은 없습니다.

논리 연산자

세 개의 논리 연산자가 있습니다: and, or, not. 이 연산자의 의미는 영어에서의 의미와 비슷합니다. 예를 들어, x > 0 and x < 10 는 x 이 0 보다 큼과 동시에 10 보다 작을 경우에만 참입니다.

n%2 == 0 or n%3 == 0 는 두 조건 중의 하나만 만족되면 참입니다. 즉, 숫자가 2 3으로 나누어 떨어지면 됩니다.

마지막으로, not 연산자는 논리식을 부정합니다. 그래서 not (x > y) 는 x > y 이 거짓일 때 참이 됩니다. 즉, x 가 y 보다 작거나 같을 때 입니다.

엄밀하게 말할 때, 논리 연산자의 피 연산자는 논리식이어야만 합니다만, 파이썬은 아주 엄밀하지는 않습니다. 0이 아닌 모든 숫자는 “참”으로 해석됩니다.

>>> 17 and True
True

이런 유연함은 유용할 수 있지만, 혼란을 야기할 수 있는 미묘한 부분들이 있기도 합니다. (무엇을 하고 있는지 알고 있지 않다면) 아마도 이런 부분을 피하고 싶을 것입니다.

조건부 실행

[conditional.execution]

쓸모 있는 프로그램을 쓰기 위해서, 우리는 언제나 조건을 검사하고, 그에 따라 프로그램의 동작을 바꿀 수 있는 능력을 필요로 합니다. 조건문은 우리에게 이 능력을 제공합니다. 가장 간단한 형태는 if 문장입니다:

if x > 0:
    print 'x 는 양수'

if 뒤에 오는 논리식을 조건이라고 부릅니다. 조건이 참이면 들여쓰기 한 문장이 실행됩니다. 그렇지 않으면 아무 일도 일어나지 않습니다.

if 문장들은 함수 정의와 동일한 구조를 갖고 있습니다: 헤더와 그 뒤를 따르는 들여쓰기 한 바디. 이런 종류의 문장들을 복문이라고 합니다.

바디에 나올 수 있는 문장의 개수에 대한 상한은 없습니다만, 적어도 하나는 있어야 합니다. 가끔, 문장이 없는 바디가 쓸모 있을 수 있습니다(보통 아직 쓰지 않은 코드를 위한 자리를 만들어 둘 때). 이런 경우, 아무런 일도 하지 않는 pass 문장을 쓸 수 있습니다.

if x < 0:
    pass          # 음수를 다룰 필요가 있음!

대안적 실행

[alternative.execution]

if 문장의 두 번째 형태는, 두 가지 가능성 중 조건이 어떤 것이 실행될지를 결정하는 대안적 실행입니다. 문법은 이렇습니다:

if x%2 == 0:
    print 'x 는 짝수'
else:
    print 'x 는 홀수'

x를 2로 나눌 때의 나머지가 0이면, x가 짝수임을 알 수 있고, 프로그램은 그 사실을 표시합니다. 만약 조건이 거짓이면, 두 번째 문장 집합이 실행됩니다. 조건은 참과 거짓 중 어느 하나일 수 밖에 없으므로, 대안들 중 정확히 하나만이 실행됩니다. 대안들을 분기라고 하는데, 실행 흐름의 지류들이기 때문입니다.

연쇄적 조건문

때로 두 개 이상의 가능성이 있어서, 두 개 이상의 분기가 필요합니다. 이런 종류의 연산을 표현하는 한가지 방법은 연쇄적 조건문입니다:

if x < y:
    print 'x 는 y 보다 작다'
elif x > y:
    print 'x 는 y 보다 크다'
else:
    print 'x 와 y 는 같다'

elif 는 “else if”의 줄임 말입니다. 다시 한번, 정확히 하나의 분기만 실행됩니다. elif 문장의 개수에는 제한이 없습니다. else 절이 있을 때는 반드시 끝에 와야 합니다만, 꼭 있을 필요는 없습니다.

if choice == 'a':
    draw_a()
elif choice == 'b':
    draw_b()
elif choice == 'c':
    draw_c()

각 조건은 순서대로 검사됩니다. 만약 첫 번째가 거짓이면, 두 번째를 검사하고, 등등. 그 중 하나가 참이면 해당 분기가 실행된 후 문장이 종료됩니다. 하나 이상의 조건이 참이라 할지라도, 오직 첫 번째로 참인 분기만이 실행됩니다.

중첩된 조건문

조건문은 다른 조건문 안으로 중첩될 수 있습니다. 3분할 예제를 이렇게 쓸 수 있습니다:

if x == y:
    print 'x 와 y 는 같다'
else:
    if x < y:
        print 'x 는 y 보다 작다'
    else:
        print 'x 는 y 보다 크다'

바깥의 조건문은 두 개의 분기를 갖고 있습니다. 첫 번째 분기는 단문으로 구성됩니다. 두 번째 분기는 그 자신 두 개의 분기를 갖고 있는 if 문장을 포함하고 있습니다. 두 분기는 모두 단문인데, 원한다면 이 들도 조건문이 될 수 있습니다.

문장들의 들여쓰기가 구조를 명확하게 만듦에도 불구하고, 중첩된 조건문은 금방 읽기 어려워집니다. 일반적으로, 할 수 있다면 피하는 것이 좋습니다.

논리 연산자는 종종 중첩된 조건문을 단순화하는 방법을 제공합니다. 예를 들어, 다음과 같은 코드를 하나의 조건문으로 다시 쓸 수 있습니다:

if 0 < x:
    if x < 10:
        print 'x 는 한자리 양수.'

print 문장은 두 조건을 모두 통과할 때만 실행되기 때문에, and 연산자를 사용해서 같은 효과를 얻을 수 있습니다:

if 0 < x and x < 10:
    print 'x 는 한자리 양수.'

재귀

[recursion]

함수가 다른 함수를 호출하는 것은 허락됩니다; 함수가 자기 자신을 호출하는 것도 허락됩니다. 왜 이게 좋은 것인지 뻔하지 않을 수 있습니다만, 프로그램이 할 수 있는 가장 마법같은 것들 중의 하나입니다. 예를 들어, 다음 함수를 살펴보세요:

def countdown(n):
    if n <= 0:
        print '발사!'
    else:
        print n
        countdown(n-1)

n 이 0 이거나 음수면, “발사!”를 출력합니다. 그렇지 않으면 n 을 인쇄하고, countdown—자신—이라는 함수를 인자 n-1을 사용하여 호출합니다.

이 함수를 이렇게 호출한다면 어떤 일이 일어날까요?

>>> countdown(3)

countdown의 실행은 n=3으로 시작하고, n은 0보다 크기 때문에, 3을 출력한 후 자신을 호출합니다...

countdown의 실행은 n=2로 시작하고, n은 0보다 크기 때문에, 2를 출력한 후 자신을 호출합니다...

countdown의 실행은 n=1로 시작하고, n은 0보다 크기 때문에, 1을 출력한 후 자신을 호출합니다...

countdown의 실행은 n=0으로 시작하고, n은 0보다 크지 않기 때문에, “발사!”를 출력한 후 돌아갑니다.

n=1을 받은 countdown이 돌아갑니다.

n=2를 받은 countdown이 돌아갑니다.

n=3을 받은 countdown이 돌아갑니다.

그런 다음 여러분은 __main__ 으로 돌아옵니다. 그래서, 출력 전체는 이렇게 됩니다:

3
2
1
발사!

자신을 호출하는 함수를 재귀적이라고 합니다; 이 과정을 재귀라고 합니다.

또 하나의 예로, 문자열을 n번 인쇄하는 함수를 쓸 수 있습니다.

def print_n(s, n):
    if n <= 0:
        return
    print s
    print_n(s, n-1)

만약 n <= 0 이면, return 문장은 함수를 종료합니다. 실행의 흐름은 즉시 호출자로 돌아가고, 함수의 나머지 문장들은 실행되지 않습니다.

함수의 남은 부분은 countdown과 비슷합니다: n 이 0 보다 크면, s 를 인쇄한 후 자신을 호출해 s 를 \(n-1\) 번만큼 더 인쇄합니다. 그래서 출력의 줄 수는 전부 1 + (n - 1) 이 되는데, 합치면 n 입니다.

이렇게 간단한 예에서는, 아마도 for 루프를 쓰는 것이 더 쉬울 겁니다. 그러나 for 루프로는 쓰기 어렵지만, 재귀로는 쉬운 예를 나중에 보게 됩니다. 그러니 일찌감치 시작해두면 좋습니다.

재귀적 함수의 스택 다이어그램

[recursive.stack]

[stackdiagram] 절에서, 함수 호출 중의 프로그램의 상태를 표시하기 위해 스택 다이어그램을 사용했습니다. 같은 종류의 다이어그램이 재귀적 함수를 해석하는데 도움을 줄 수 있습니다.

함수가 호출될 때마다, 파이썬은 함수의 지역 변수와 매개변수를 담는 새 함수 프레임을 만듭니다. 재귀적 함수의 경우 특정 시점에 스택에는 하나 이상의 프레임이 있을 수 있습니다.

그림 [fig.stack2] 은 n = 3으로 호출된 countdown 의 스택 다이어그램을 보여줍니다.

stack2

[fig.stack2]

보통 스택의 꼭대기는 __main__ 의 프레임 입니다. __main__ 에 어떤 변수도 만들지 않았고, 인자도 넘겨주지 않았기 때문에 비어있습니다.

네개의 countdown 프레임은 서로 다른 n의 값들을 갖고 있습니다. 스택의 바닥, n=0, 은 기저 사례라고 불립니다. 기저 사례는 재귀적 호출을 하지 않기 때문에, 더 이상의 프레임은 없습니다.

[연습 5.1.] s = 'Hello' 와 n=2 로 호출되는 print_n 의 스택 다이어그램을 그리세요.

[연습 5.2.] 함수 객체와 숫자 n 을 인자로 받아들여서, 함수를 n 번 호출하는 함수 do_n을 쓰세요.

무한 재귀

만약 재귀가 기저 사례에 도달하지 못한다면, 영원히 재귀 호출을 하게 되고, 프로그램은 끝나지 않습니다. 이것이 무한 재귀로 알려져 있는 것이고, 일반적으로 좋은 생각이 아닙니다. 여기 무한 재귀를 포함하는 최소 프로그램이 있습니다:

def recurse():
    recurse()

대부분의 프로그래밍 환경에서, 무한 재귀를 포함한 프로그램이 정말로 영원히 실행되지는 않습니다. 파이썬은 최대 재귀 깊이에 도달할 경우 오류 메시지를 보고합니다:

  File "<stdin>", line 2, in recurse
  File "<stdin>", line 2, in recurse
  File "<stdin>", line 2, in recurse
                  .
                  .
                  .
  File "<stdin>", line 2, in recurse
RuntimeError: Maximum recursion depth exceeded

이 트래이스백은 앞의 장에서 본 것보다 약간 큽니다. 오류가 발생할 때 스택에는 100개의 recurse 프레임이 있습니다!

자판 입력

아직까지 우리가 쓴 프로그램들은 사용자로부터 아무런 입력도 받지 않는다는 점에서 좀 어설픕니다. 매번 같은 것을 수행할 뿐입니다.

파이썬 2는 자판으로부터 입력을 받아들이는 raw_input 이라는 내장 함수를 제공합니다. 파이썬 3에서는 input 라고 불립니다. 이 함수가 호출되면, 프로그램은 멈춰 서서 사용자가 뭔가를 입력할 때까지 기다립니다. 사용자가 Return 이나 Enter를 누르면, 프로그램은 복귀하고 raw_input 은 사용자가 입력한 것을 문자열로 돌려줍니다.

>>> text = raw_input()
What are you waiting for?
>>> print text
What are you waiting for?

사용자로부터 입력을 얻기 전에, 사용자에게 무엇을 입력할지를 알리는 프롬프트를 인쇄하는 것이 좋습니다. raw_input 는 인자로 프롬프트를 받아들일 수 있습니다:

>>> name = raw_input('What...is your name?\n')
What...is your name?
Arthur, King of the Britons!
>>> print name
Arthur, King of the Britons!

프롬프트의 끝에 있는 \n 문자열은 줄 넘김을 나타내는데, 줄을 끝내도록 하는 특수 문자입니다. 이 때문에 사용자의 입력은 프롬프트 아래에 나타나게 됩니다.

사용자가 정수를 입력할 것으로 기대한다면, 반환 값을 int로 변환할 수 있습니다:

>>> prompt = 'What...is the airspeed velocity of an unladen swallow?\n'
>>> speed = raw_input(prompt)
What...is the airspeed velocity of an unladen swallow?
17
>>> int(speed)
17

하지만 사용자가 숫자로 이루어진 문자열외에 다른 것을 입력하면, 오류를 만나게 됩니다:

>>> speed = raw_input(prompt)
What...is the airspeed velocity of an unladen swallow?
What do you mean, an African or a European swallow?
>>> int(speed)
ValueError: invalid literal for int()

나중에 이런 종류의 오류를 어떻게 처리할지 보게 됩니다.

디버깅

[whitespace]

오류가 발생했을 때 파이썬이 인쇄하는 트래이스백에는 많은 정보가 담겨있습니다, 하지만 이는 과도할 수 있는데, 특히 스택에 프레임이 많은 경우 그렇습니다. 보통 이 것들이 가장 유용합니다:

  • 오류의 종류와
  • 발생한 장소.

문법 오류는 보통 찾기 쉽습니다만 몇 가지 까다로운 경우가 있습니다. 공백과 탭은 보이지 않고, 일상적으로 무시하기 때문에, 공백문자 오류는 까다로울 수 있습니다.

>>> x = 5
>>>  y = 6
  File "<stdin>", line 1
    y = 6
    ^
SyntaxError: invalid syntax

이 예에서, 문제는 두 번째 줄이 한 칸 들여쓰기 한 것입니다. 하지만 오류 메시지는 y를 가리킴으로써 우리를 잘못 인도합니다. 일반적으로, 오류 메시지는 문제가 발견된 곳을 가리킵니다만, 실제 오류는 코드의 더 앞에, 때로 앞 줄에, 있을 수 있습니다.

실행시간 오류도 마찬가지 입니다.

신호 대 잡음 비를 데시벨로 계산하려고 한다고 가정합시다. 식은 \(SNR_{db} = 10 \log_{10} (P_{signal} / P_{noise})\) 입니다. 파이썬에서 이런 식으로 쓸 수 있습니다:

import math
signal_power = 9
noise_power = 10
ratio = signal_power / noise_power
decibels = 10 * math.log10(ratio)
print decibels

하지만 파이썬 2에서 실행하면, 오류 메시지를 만납니다.

Traceback (most recent call last):
  File "snr.py", line 5, in ?
    decibels = 10 * math.log10(ratio)
ValueError: math domain error

오류 메시지는 5번 줄을 가리킵니다만, 그 줄에는 잘못된 것이 없습니다. 실제 오류를 찾기 위해서, ratio의 값을 인쇄해 보는 것이 유용한데, 0인 것으로 밝혀집니다. 문제는 4번 줄에 있는데, 두 개의 정수를 사용하면 정수 나눗셈이 이루어지기 때문입니다. 해결책은 signalpower 와 noisepower를 실수 값으로 표현하는 것입니다.

일반적으로, 오류 메시지는 문제가 발견된 지점을 가리킵니다만, 종종 문제가 발생한 곳이 아닙니다.

파이썬 3에서, 이 예는 오류를 발생시키지 않습니다; 나눗셈 연산자는 정수 피 연산자가 올 때도 실수 나눗셈을 수행합니다.

용어

나머지 연산자 modulus operator:
정수에 작용하여 한 숫자를 다른 숫자로 나눈 나머지를 제공하는, 백분율 기호(%)로 표시되는 연산자.
논리식 boolean expression:
값이 True 또는 False 인 표현식.
비교 연산자 relational operator:
피 연산자들을 비교하는 연산자들: ==, !=, >, <, >=, <=.
논리 연산자 logical operator:
논리식을 조합하는 연산자들: and, or, not.
조건문 conditional statement:
어떤 조건에 따라 실행 흐름을 제어하는 문장.
조건 condition:
조건문에 있는 표현식으로 어떤 분기가 실행될지 결정한다.
복문 compound statement:
헤더와 바디로 구성된 문장. 헤더는 콜론 (:)으로 끝난다. 바디는 헤더에 상대적으로 들여쓰기 한다.
분기 branch:
조건문의 대안적인 문장 집합.
연쇄적 조건문 chained conditional:
연속적인 대안적 분기를 갖는 조건문.
중첩된 조건문 nested conditional:
다른 조건문의 분기중 한 곳에 등장하는 조건문.
재귀 recursion:
현재 실행중인 함수를 호출하는 과정.
기저 사례 base case:
재귀적 함수의 분기중에서 재귀적 호출을 하지 않는 것.
무한 재귀 infinite recursion:
기저 사례를 갖지 않거나, 그 곳에 도달하지 못하는 재귀. 결국, 무한 재귀는 실행시간 오류를 일으킨다.

연습

[연습 5.3.]

페르마의 마지막 정리는, 2보다 큰 모든 \(n\) 에 대해

\[a^n + b^n = c^n\]

를 만족하는 정수 \(a\) , \(b\) , \(c\) 가 존재하지 않는다고 합니다.

  1. 네 개의 인자—a, b, c, n—를 받아들여 페르마의 정리가 성립하는지 검사하는 함수 check_fermat를 작성하세요. 만약 \(n\) 이 2보다 크고 다음 식을 만족하는 것으로 밝혀지면, 프로그램은 “페르마가 틀렸다!”를 인쇄해야 합니다.

    \[a^n + b^n = c^n\]

    그렇지 않으면 “아냐, 그건 아니지.”를 인쇄합니다.

  2. 사용자에게 a, b, c, n의 값들을 입력하도록 요청하고, 정수로 변환한 후, check_fermat를 사용해서 페르마의 정리를 반증하는지 검사하는 함수를 작성하세요.

[연습 5.4.]

세 개의 막대가 주어질 때 그들을 배치해서 삼각형을 만들 수도 있고 이렇지 못할 수도 있습니다. 예를 들어, 한 막대의 길이가 12인치이고, 나머지 두 막대가 모두 1인치라면, 짧은 막대들을 중간에서 만나게 만들 방법이 없음은 자명합니다. 임의의 세 길이에 대해, 삼각형을 이룰 수 있을지 간단히 검사할 수 있습니다.

만약 막대 하나의 길이가 다른 두 막대의 합보다 크다면, 삼각형을 만들 수 없습니다. 그렇지 않다면 만들 수 있습니다. (만약 두 길이의 합이 세 번째와 같다면, “퇴화(degenerate)” 삼각형이라고 합니다.)
  1. 세 개의 정수를 인자로 받아들여, 주어진 길이의 막대로 삼각형을 만들 수 있는지 여부에 따라, “Yes” 나 “No”를 인쇄하는 함수 is_triangle을 작성하세요.
  2. 사용자에게 세 막대의 길이를 입력하도록 요청하고, 정수로 변환한 후, is_triangle를 사용해 주어진 길이의 막대들이 삼각형을 이룰 수 있는지 검사하는 함수를 쓰세요.

다음 연습은 [turtlechap] 장의 TurtleWorld 를 사용합니다.

[연습 5.5.]

다음 함수를 읽고 무엇을 하는지 추측해보세요. 그런 다음 실행해 보세요([turtlechap] 장의 연습을 보세요).

def draw(t, length, n):
    if n == 0:
        return
    angle = 50
    fd(t, length*n)
    lt(t, angle)
    draw(t, length, n-1)
    rt(t, 2*angle)
    draw(t, length, n-1)
    lt(t, angle)
    bk(t, length*n)

koch

[fig.koch]

[연습 5.6.]

코흐 곡선(Koch curve)은 그림 [fig.koch]처럼 보이는 프랙탈입니다. 길이 \(x\) 의 코흐 곡선을 그리려면, 이렇게 하면 됩니다:

  1. 길이 \(x/3\) 의 코흐 곡선을 그립니다.
  2. 60도 좌회전합니다.
  3. 길이 \(x/3\) 의 코흐 곡선을 그립니다.
  4. 120도 우회전합니다.
  5. 길이 \(x/3\) 의 코흐 곡선을 그립니다.
  6. 60도 좌회전합니다.
  7. 길이 \(x/3\) 의 코흐 곡선을 그립니다.

예외는 \(x\) 가 3보다 작을 때입니다: 그 경우에는, 단지 길이 \(x\) 의 직선을 그리면 됩니다.

  1. 거북이와 길이를 매개변수로 받아서, 거북이로 주어진 길이의 코흐 곡선을 그리는 함수 koch를 작성하세요.

  2. 세 개의 코흐 곡선을 사용해서 눈송이의 둘레를 그리는 함수 snowflake를 작성하세요.

    답: http://thinkpython.com/code/koch.py.

  3. 코흐 곡선은 여러 가지 방법으로 일반화할 수 있습니다. http://en.wikipedia.org/wiki/Koch_snowflake 에서 예를 찾아보고 가장 좋아하는 것을 구현하세요.