반복

다중 대입

아마도 이미 발견했겠지만, 같은 변수에 여러번 대입하는 것은 올바른 용법입니다. 새 대입은 이미 만들어진 변수가 새 값을 가리키도록 만듭니다 (그리고 더 이상 예전 값을 가리키지는 않습니다).

bruce = 5
print bruce,
bruce = 7
print bruce

bruce 가 처음 인쇄될 때 값은 5이고, 두 번째에는 값이 7이기 때문에, 이 프로그램의 출력은 5 7 이 됩니다. 첫 번째 print 문의 끝에 붙은 콤마는 줄 넘김을 막는데, 이것이 두 출력이 같은 줄에 나오는 이유입니다.

그림 [fig.assign2]은 상태 도에서 다중 대입이 어떻게 나타나는지 보여줍니다.

다중 대입과 관련하여, 대입 연산과 항등식간의 구별은 특히 중요해집니다. 파이썬이 대입에 등호(=)를 사용하기 때문에, a = b 와 같은 문장을 항등식으로 해석하고 싶습니다. 그렇지 않습니다!

첫째로, 항등은 대칭적인 관계지만 대입은 그렇지 않습니다. 예를 들어, 수학에서, \(a=7\)\(7=a\) 입니다. 그러나 파이썬에서, a = 7 는 옳지만 7 = a 는 그렇지 못합니다.

더 나아가, 수학에서, 항등식은 처음부터 끝까지 참이거나 거짓입니다. 지금 \(a=b\) 이면, \(a\) 는 항상 \(b\) 와 같습니다. 파이썬에서, 대입 문이 두 변수를 같게 만들 수 있습니다만, 그들이 계속 그렇게 유지될 필요는 없습니다:

a = 5
b = a    # a 와 b 는 지금 같습니다
a = 3    # a 와 b 는 더 이상 같지 않습니다

세 번째 줄은 a의 값을 바꾸지만 b의 값을 바꾸지는 않기 때문에, 그들은 더 이상 같지 않습니다.

다중 대입이 종종 도움이 되기는 하지만, 주의해서 사용해야 합니다. 변수의 값이 자주 바뀌면, 코드를 읽고 디버깅하기 어렵게 됩니다.

assign2

[fig.assign2]

변수 갱신

[update]

다중 대입의 가장 흔한 형태 중 하나는 갱신인데, 변수의 새 값이 예전 값에 의존하는 경우입니다.

x = x+1

“x의 값을 취해서, 1을 더한 후에, 새 값으로 x를 갱신하라”는 뜻입니다.

존재하지 않는 변수를 갱신하려고 하면 오류를 만나게 되는데, 파이썬이 값을 x에 대입하기 전에 우변을 먼저 계산하기 때문입니다:

>>> x = x+1
NameError: name 'x' is not defined

변수를 갱신할 수 있기 전에, 초기화해야만 하는데, 보통 단순한 대입을 사용합니다:

>>> x = 0
>>> x = x+1

변수 갱신 중에 1을 더하는 것을 증가(increment); 1을 빼는 것을 감소(decrement)라고 부릅니다.

while 문

컴퓨터는 종종 반복적인 작업을 자동화하는데 사용됩니다. 같거나 비슷한 작업들을 오류 없이 반복하는 것은 컴퓨터가 잘하고 사람이 못하는 것입니다.

반복을 수행하기 위해 재귀를 사용하는 두 프로그램 countdown 과 print_n 을 본적이 있습니다. 반복은 아주 흔하기 때문에, 파이썬은 편의를 제공하기 위해 몇 가지 언어적 기능들을 제공합니다. 하나는 [repetition] 절 에서 본 for 문입니다. 뒤에서 다시 돌아가게 될 것입니다.

다른 하나는 while 문입니다. 여기 while 문을 사용하는 countdown 의 버전이 있습니다:

def countdown(n):
    while n > 0:
        print n
        n = n-1
    print '발사!'

여러분은 while 문을 거의 영어처럼 읽을 수 있습니다. 이런 뜻입니다, “n 이 0 보다 큰 동안(while), n의 값을 출력하고 n의 값을 1 줄인다. 0이 될 때, 발사!를 출력한다.”.

좀더 형식적으로, 여기 while 문의 실행 흐름이 있습니다:

  1. 조건을 계산해서, True 나 False를 얻는다.
  2. 조건이 거짓이면, while 문을 빠져나가 다음 문장부터 계속 실행한다.
  3. 조건이 참이면, 바디를 실행하고 1 단계로 돌아간다.

이런 식의 흐름을 순환(loop) 이라고 하는데, 세 번째 단계가 처음으로 되돌아가기 때문입니다.

순환의 바디는 하나나 그 이상의 변수들의 값을 수정해서, 결국 조건이 거짓이 되어 순환이 종료하도록 만들어야 합니다. 그렇지 않으면 순환은 영원히 반복되는데, 이를 무한 순환이라고 부릅니다. 컴퓨터 과학자의 끝없는 즐거움의 원천 중 하나는 샴푸의 사용법, “거품 내고, 씻고, 반복하세요”가 무한 순환이라는 관찰입니다.

countdown 의 경우에, 순환이 끝남을 증명할 수 있는데, n 의 값이 유한함을 알고 있고, 매 순환마다 n의 값이 점점 작아짐을 확인할 수 있어서, 결국 0을 얻게 되기 때문입니다. 다른 경우에는, 말하기가 그렇게 쉽지는 않습니다:

def sequence(n):
    while n != 1:
        print n,
        if n%2 == 0:        # n 은 짝수
            n = n/2
        else:               # n 은 홀수
            n = n*3+1

이 순환의 조건은 n != 1 이라서, 순환은 조건을 거짓으로 만들도록 n 이 1 이 될 때까지 계속될 것입니다.

매 순환마다, 프로그램은 n의 값을 출력하고 짝수인지 홀수인지를 검사합니다. 짝수면 n 을 2로 나눕니다. 홀수면 n 의 값은 n*3+1로 바뀝니다. 예를 들어, sequence 에 전달된 인자가 3이면, 만들어지는 수열은 3, 10, 5, 16, 8, 4, 2, 1 입니다.

n 은 때로 증가하고 때로 감소하기 때문에, n 이 결국 1 에 도달할지, 또는 프로그램이 종료할지에 관한 확실한 증명은 없습니다. 특정한 n 의 값에 대해서는, 종료를 증명할 수 있습니다. 예를 들어, 시작 값이 2의 거듭제곱이면 n 의 값은 1 에 도달할 때까지 매 순환마다 짝수가 됩니다. 앞의 예는 16으로 시작하는 같은 종류의 수열로 끝납니다.

어려운 질문은 n 의 모든 양의 값에 대해 프로그램이 종료하는지 증명할 수 있는가 입니다. 지금까지는, 아무도 증명하지도 반증하지도 못했습니다! (http://en.wikipedia.org/wiki/Collatz_conjecture를 보세요.)

[연습 7.1.]

[recursion] 절에 나온 print_n 함수를 재귀 대신에 반복을 사용하도록 다시 쓰세요.

break

때로 바디의 중간까지 갈 때까지 순환을 끝낼 때인지를 모를 수 있습니다. 그런 경우에 순환에서 빠져나가는 break 문을 사용할 수 있습니다.

예를 들어, 사용자로부터 done 가 올 때까지 입력을 받는다고 가정하세요. 이렇게 쓸 수 있습니다:

while True:
    line = raw_input('> ')
    if line == 'done':
        break
    print line

print 'Done!'

순환 조건이 True, 항상 참입니다, 라서 순환은 break 문을 만날 때까지 실행됩니다.

매 반복마다, 사용자에게 꺾쇠괄호(angle bracket)를 제시합니다. 사용자가 done을 입력하면, break 문이 순환을 빠져나가게 합니다. 그렇지 않으면 프로그램은 사용자가 입력한 것을 되돌려주고, 순환의 처음으로 돌아갑니다. 여기 실행 예가 있습니다:

> not done
not done
> done
Done!

이런 식으로 while 순환을 사용하는 경우는 흔한데, 순환의 (처음이 아니라) 어느 위치에서든 조건을 검사할 수 있고, 종료 조건을 부정적(“조건이 성립하는 동안 계속해”)이기 보다 긍정적(“조건이 성립하면 멈춰”)으로 표현할 수 있기 때문입니다.

제곱근

[squareroot]

순환은 종종 근사값으로 출발한 후 반복적으로 개선해가는 방식으로 수치 계산을 하는 프로그램에서 사용됩니다.

예를 들어, 제곱근을 계산하는 한가지 방법은 뉴톤법(Newton’s method)입니다. 여러분이 \(a\) 의 제곱근을 알고 싶어한다고 가정하세요. 여러분이 어떤 추정, \(x\), 에서 출발하더라도, 다음 식을 사용해서 더 개선된 추정을 계산할 수 있습니다:

\[y = \frac{x + a/x}{2}\]

예를 들어, \(a\) 가 4이고 \(x\) 가 3이면:

>>> a = 4.0
>>> x = 3.0
>>> y = (x + a/x) / 2
>>> print y
2.16666666667

이 값은 정답 (\(\sqrt{4} = 2\)) 에 좀 더 가깝습니다. 새 추정으로 이 과정을 반복하면, 점점 더 가까워 집니다:

>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00641025641

몇번 더 갱신하면, 추정은 거의 정확해집니다:

>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00001024003
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.00000000003

일반적으로 정답에 도달하는데 얼마나 많은 단계가 필요한지 미리 알지 못하지만, 추정의 바뀌지 않기 때문에 도달했을 때 알 수 있습니다:

>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0
>>> x = y
>>> y = (x + a/x) / 2
>>> print y
2.0

y == x 일 때 멈출 수 있습니다. 여기 초기 추정, x,으로 출발한 후, 바뀌지 않을 때까지 개선하는 순환이 있습니다:

while True:
    print x
    y = (x + a/x) / 2
    if y == x:
        break
    x = y

대부분의 a 의 값에 대해 이 순환은 잘 동작합니다만, 일반적으로 실수(float) 등가(equality)를 검사하는 것은 위험합니다. 부동소수 값은 단지 근사적으로만 맞습니다: 대부분의 유리수, \(1/3\) 같은,와 무리수, \(\sqrt{2}\) 같은,는 float로 정확하게 표현될 수 없습니다.

x 와 y가 정확히 같은지를 검사하기 보다는, 내장 함수 abs를 사용해서 둘 간의 차의 절대값, 또는 크기,을 계산하는 것이 더 안전합니다:

if abs(y-x) < epsilon:
    break

epsilon은 0.0000001 같은 식의 값인데, 얼마나 가까워야 충분히 가까운지를 결정합니다.

[연습 7.2.]

이 순환을 square_root 라는 이름의 함수로 캡슐화하세요. 함수는 a를 매개변수로 받아들이고, 합리적인 x 값을 선택한 후, a 의 제곱근에 대한 추정을 돌려줍니다.

알고리즘

뉴톤법은 알고리즘의 한 예인데, 한 부류의 문제를 풀 수 있는 기계적인 과정 (이 경우에는, 제곱근의 계산)을 뜻합니다.

알고리즘을 정의하는 것은 쉽지 않습니다. 알고리즘이 아닌 것으로 시작하는 것이 도움이 될 수 있습니다. 한자리 숫자의 곱셈을 배울 때, 여러분은 아마도 구구단을 암기했을 겁니다. 결과적으로, 여러분은 100가지 개별 해법을 기억한 것입니다. 이런 종류의 지식은 알로그즘적이지 않습니다.

하지만 여러분이 “게을렀다면”, 아마도 몇 가지 요령을 배우는 잔꾀를 부렸을 겁니다. 예를 들어, \(n\) 과 9 의 곱을 구하기 위해, 첫 숫자로 \(n-1\) 를, 두 번째 숫자로 \(10-n\) 를 쓸 수 있습니다. 이 요령은 임의의 한자리 숫자와 9의 곱에 관한 일반적인 해법입니다. 이 것은 알고리즘입니다!

비슷하게, 올림을 사용한 덧셈, 빌려오기를 사용한 뺄셈, 큰 수의 나눗셈(long division)에서 배운 기술들은 모두 알고리즘입니다. 알고리즘의 한가지 특징은 수행하는데 지능을 요구하지 않는다는 것입니다. 단순한 규칙에 따라 각 단계를 밟아가는 기계적인 과정입니다.

제 견해로는, 사람이 학교에서 그렇게 많은 시간을, 지능을 요구하지 않는 알고리즘을 실행하는 법을 배우는데 쓰고 있다는 것은 부끄러운 일입니다.

반면에, 알고리즘을 설계하는 과정은 흥미롭고, 지적으로 도전적이며, 우리가 프로그래밍이라고 부르는 것의 핵심요소입니다.

사람들이 자연스럽고 어려움이나 의식적인 생각 없이 행하는 것들 중 몇몇은 알고리즘적으로 표현하기 가장 어려운 것들입니다. 자연어 해석이 좋은 예입니다. 우리 모두 그 것을 하고 있지만, 아직 아무도 우리가 어떻게 하는지 설명하지 못하고 있습니다, 적어도 알고리즘의 형태로는.

디버깅

더 큰 프로그램을 쓰기 시작함에 따라, 더 많은 시간을 디버깅에 쓰고 있는 자신을 발견하게 될 것입니다. 더 많은 코드는 오류를 만들 더 많은 기회와 버그가 숨을 수 있는 더 많은 장소를 의미합니다.

디버깅 시간을 줄이는 한가지 방법은 “양분법을 사용한 디버깅”입니다. 예를 들어, 프로그램에 100 줄이 있고 한번에 한 줄씩 검토한다면, 100 단계가 필요하게 됩니다.

대신에, 프로그램을 반을 나눠보세요. 프로그램의 중간이나 그 근처에서 검사할 수 있는 중간 값을 찾으세요. print 문(또는 검사할 수 있는 효과를 갖는 다른 어떤 것)을 넣고 프로그램을 실행하세요.

만약 중간 지점 검사가 틀린다면, 프로그램의 전반부에 문제가 있어야만 합니다. 올바르다면, 문제는 후반부에 있습니다.

이런 식의 검사를 수행할 때마다, 검색해야 할 줄 수를 반씩 나누세요. 여섯 단계 (1100보다 작은 숫자 입니다) 후에는 한두 줄의 코드로 좁혀지게 됩니다, 적어도 이론적으로는.

실제로는 “프로그램의 중간”이 항상 명확한 것은 아니고, 검사가 항상 가능한 것도 아닙니다. 줄 수를 샌 후 정확히 중간 지점을 찾는 것은 말이 안됩니다. 대신에, 프로그램에서 오류가 있을 법한 장소들과, 검사를 넣기에 좋은 장소들을 생각하세요. 그런 다음 버그가 검사 전과 후에 있을 가능성이 비슷해 보이는 지점을 고르세요.

용어

다중 대입 multiple assignment:
프로그램이 실행되는 동안 같은 변수에 한 번 이상의 대입을 수행하는 것.
갱신 update:
변수의 새 값이 예전 값에 의존하는 대입.
초기화 initialization:
나중에 갱신될 변수에 초기 값을 제공하는 대입.
증가 increment:
변수의 값을 (종종 1 만큼) 크게 만드는 갱신.
감소 decrement:
변수의 값을 작게 만드는 갱신.
반복 iteration:
재귀적 함수 호출이나 순환을 사용해서 일련의 문장들을 여러 번 수행하는 것.
무한 순환 infinite loop:
종료 조건이 영원히 만족되지 못하는 순환.

연습

[연습 7.3.]

이 장의 제곱근 알고리즘을 검사하기 위해, math.sqrt 와 비교할 수 있습니다. 이런 식의 표를 인쇄하는 test_square_root 라는 이름의 함수를 작성하세요.

1.0 1.0           1.0           0.0
2.0 1.41421356237 1.41421356237 2.22044604925e-16
3.0 1.73205080757 1.73205080757 0.0
4.0 2.0           2.0           0.0
5.0 2.2360679775  2.2360679775  0.0
6.0 2.44948974278 2.44948974278 0.0
7.0 2.64575131106 2.64575131106 0.0
8.0 2.82842712475 2.82842712475 4.4408920985e-16
9.0 3.0           3.0           0.0

첫 번째 열은 숫자, \(a\); 두 번째 열은 [squareroot] 절에 나온 함수로 계산한 \(a\) 이 제곱근; 세 번째 열은 math.sqrt 로 계산한 제곱근; 네 번째 열은 두 추정간의 차의 절대값입니다.

[연습 7.4.]

내장함수 eval는 문자열을 받아들여서, 그 값을 파이썬 인터프리터로 계산합니다. 예를 들어:

>>> eval('1 + 2 * 3')
7
>>> import math
>>> eval('math.sqrt(5)')
2.2360679774997898
>>> eval('type(math.pi)')
<type 'float'>

반복적으로 사용자에게 입력을 요청해서, eval 로 계산한 후 결과를 인쇄하는 함수 eval_loop 를 작성하세요.

사용자가 'done' 을 입력할 때까지 계속해야 하고, 계산한 마지막 표현식의 값을 돌려줘야 합니다.

[연습 7.5.]

수학자 스리니바사 라마누잔(Srinivasa Ramanujan)은 \(\pi\) 의 근사값을 만드는데 사용할 수 있는 무한 수열을 발견했습니다:

\[\frac{1}{\pi} = \frac{2\sqrt{2}}{9801} \sum^\infty_{k=0} \frac{(4k)!(1103+26390k)}{(k!)^4 396^{4k}}\]

이 식으로 \(\pi\) 의 추정을 계산해서 돌려주는 함수 estimate_pi 을 작성하세요. while 순환을 사용해서 항의 합을 계산해야 하는데, 마지막 항이 1e-15 (파이썬 식 표현으로는 \(10^{-15}\)) 보다 작아질 때까지 계속해야 합니다. math.pi 와 비교해서 결과를 검사할 수 있습니다.

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