6 . 반복(Iteration)

6.1 변수 갱신

대입문의 흔한 패턴은 변수를 갱신하는 대입문이다. 변수의 새로운 값은 이전 값에 의존한다.

x <- x + 1

상기 예제는 “현재 값 x에 1을 더해서 x를 새로운 값으로 갱신한다.”

만약 존재하지 않는 변수를 갱신하면, 오류가 발생한다. 왜냐하면 x에 값을 대입하기 전에 R이 오른쪽을 먼저 평가하기 때문이다.

x <- x + 1
Error: object 'x' not found

변수를 갱신하기 전에 간단한 변수 대입으로 통상 먼저 초기화(initialize)한다.

x <- 0
x <- x + 1

1을 더해서 변수를 갱신하는 것을 증가(increment)라고 하고, 1을 빼서 변수를 갱신하는 것을 감소(decrement)라고 한다.

6.2 while문

종종 반복적인 작업을 자동화하기 위해서 컴퓨터를 사용한다. 오류 없이 동일하거나 비슷한 작업을 반복하는 일은 컴퓨터가 사람보다 잘한다. 반복이 매우 흔한 일이어서, R에서 반복 작업을 쉽게 하도록 몇가지 언어적 기능을 제공한다.

R에서 반복의 한 형태가 while문이다. 다음은 5 에서부터 거꾸로 세어서 마지막에 “Blastoff(발사)!”를 출력하는 간단한 프로그램이다.

n <- 5
while(n > 0) {
  print(n)
  n <- n -1
}
## [1] 5
## [1] 4
## [1] 3
## [1] 2
## [1] 1
print("Blastoff(발사)!")
## [1] "Blastoff(발사)!"

마치 영어를 읽듯이 while을 읽어 내려갈 수 있다. n이 0 보다 큰 동안에 n의 값을 출력하고 n 값에서 1만큼 뺀다. 0 에 도달했을 때, while문을 빠져나가 Blastoff(발사)!“를 화면에 출력한다.

좀더 형식을 갖춰 정리하면, 다음이 while문에 대한 실행 흐름에 대한 정리다.

  1. 조건을 평가해서 참(TRUE) 혹은 거짓(FALSE)를 산출한다.
  2. 만약 조건이 거짓이면, while문을 빠져나가 다음 문장을 계속 실행한다.
  3. 만약 조건이 참이면, 몸통 부문의 문장을 실행하고 다시 처음 1번 단계로 돌아간다.

3번째 단계에서 처음으로 다시 돌아가는 반복을 하기 때문에 이런 종류의 흐름을 루프(loop)라고 부른다. 매번 루프 몸통 부문을 실행할 때마다, 이것을 반복(iteration)이라고 한다. 상기 루프에 대해서 “5번 반복했다”고 말한다. 즉, 루프 몸통 부문이 5번 수행되었다는 의미가 된다.

루프 몸통 부문은 필히 하나 혹은 그 이상의 변수값을 바꾸어서 종국에는 조건식이 거짓(FALSE)이 되어 루프가 종료되게 만들어야 한다. 매번 루프가 실행될 때마다 상태를 변경하고 언제 루프가 끝날지 제어하는 변수를 반복 변수(iteration variable)라고 한다. 만약 반복 변수가 없다면, 루프는 영원히 반복될 것이고, 결국 무한 루프(infinite loop)에 빠질 것이다.

6.3 무한 루프

프로그래머에게 무한한 즐거움의 원천은 아마도 “거품내고, 헹구고, 반복” 이렇게 적혀있는 샴프 사용법 문구가 무한루프라는 것을 알아차릴 때일 것이다. 왜냐하면, 얼마나 많이 루프를 실행해야 하는지 말해주는 반복 변수(iteration variable)가 없어서 무한 반복하기 때문입니다.

숫자를 꺼꾸로 세는 (countdown) 예제는 루프가 끝나는 것을 증명할 수 있다. 왜냐하면 n값이 유한하고, n이 매번 루프를 돌 때마다 작아져서 결국 0 에 도달할 것이기 때문이다. 다른 경우 반복 변수가 전혀 없어서 루프가 명백하게 무한 반복한다.

6.4 무한 반복과 break

가끔 몸통 부문을 절반 진행할 때까지 루프를 종료해야하는 시점인지 확신하지 못한다. 이런 경우 의도적으로 무한 루프를 작성하고 break 문을 사용하여 루프를 빠져 나온다.

다음 루프는 명백하게 무한 루프(infinite loop)가 되는데 이유는 while문 논리 표현식이 단순히 논리 상수 참(TRUE)으로 되어 있기 때문이다.

n <- 10

while(TRUE) {
  print(n)
  n <- n - 1
}

print('Done!')

실수하여 상기 프로그램을 실행한다면, 폭주하는 R 프로세스를 어떻게 멈추는지 빨리 배우거나, 컴퓨터의 전원 버튼이 어디에 있는지 찾아야 할 것이다. 표현식 상수 값이 참(TRUE)이라는 사실로 루프 상단 논리 연산식이 항상 참 값이여서 프로그램이 영원히 혹은 배터리가 모두 소진될 때까지 실행된다.

이것이 역기능 무한 루프라는 것은 사실이지만, 유용한 루프를 작성하기 위해는 이 패턴을 여전히 이용할 것이다. 이를 위해서 루프 몸통 부문에 break문을 사용하여 루프를 빠져나가는 조건에 도달했을 때, 루프를 명시적으로 빠져나갈 수 있도록 주의깊게 코드를 추가해야 한다.

예를 들어, 사용자가 done을 입력하기 전까지 사용자로부터 입력값을 받는다고 가정해서 프로그램 코드를 다음과 같이 작성한다.

while(TRUE) {
  line <- readline(prompt = '> ')
  if(line == 'done') {
    break
  }
  print(line)
}

루프 조건이 항상 참(TRUE)이여서 break문이 호출될 때까지 루프는 반복적으로 실행된다.

매번 프로그램이 꺾쇠 괄호로 사용자에게 명령문을 받을 준비를 한다. 사용자가 done을 타이핑하면, break문이 실행되어 루프를 빠져나온다. 그렇지 않은 경우 프로그램은 사용자가 무엇을 입력하든 메아리처럼 입력한 것을 그대로 출력하고 다시 루프 처음으로 되돌아 간다. 다음 예제로 실행한 결과가 있다.

> hello there
hello there
> finished
finished
> done
> done
Error: object 'done' not found

while 루프를 이와 같은 방식으로 작성하는 것이 흔한데 프로그램 상단에서 뿐만 아니라 루프 어디에서나 조건을 확인할 수 있고 피동적으로 “이벤트가 발생할 때까지 계속 실행” 대신에, 적극적으로 “이벤트가 생겼을 때 중지”로 멈춤 조건을 표현할 수 있다.

6.5 next로 반복 종료

때때로 루프를 반복하는 중간에서 현재 반복을 끝내고, 다음 반복으로 즉시 점프하여 이동하고 싶을 때가 있다. 현재 반복 루프 몸통 부분 전체를 끝내지 않고 다음 반복으로 건너뛰기 위해서 next문을 사용한다.

사용자가 “done”을 입력할 때까지 입력값을 그대로 복사하여 출력하는 루프 예제가 있다. 하지만 R 주석문처럼 해쉬(#)로 시작하는 줄은 출력하지 않느다.

while(TRUE) {
  line <- readline(prompt = '> ')
  if(substr(line,1,1) == "#") {
    next
  }
  if(line == 'done') {
    break
  }
  print(line)
}

next문이 추가된 새로운 프로그램을 샘플로 실행했다.

> hello there
[1] hello there
> # don't print this
> print this!
[2] print this!
> done

해쉬 기호(#)로 시작하는 줄을 제외하고 모든 줄을 출력한다. 왜냐하면, next문이 실행될 때,현재 반복을 종료하고 while문 처음으로 돌아가서 다음 반복을 실행하게 되어서 print문을 건너뛴다.

6.6 for문 사용 명확한 루프

때때로, 단어 리스트나, 파일의 줄, 숫자 리스트 같은 사물의 집합에 대해 루프를 반복할 때가 있다. 루프를 반복할 사물 리스트가 있을 때, for문을 사용해서 확정 루프(definite loop)를 구성한다.

while문을 불확정 루프(indefinite loop)라고 하는데, 왜냐하면 어떤 조건이 거짓(FALSE)가 될 때까지 루프가 단순히 계혹해서 돌기 때문이다. 하지만, for루프는 확정된 항목의 집합에 대해서 루프가 돌게 되어서 집합에 있는 항목만큼만 실행이 된다.

for문이 있고, 루프 몸통 부문으로 구성된다는 점에서 for루프 구문은 while루프 구문과 비슷하다.

friends <- c('Joseph', 'Glenn', 'Sally')

for(friend in friends) {
  cat('Happy New Year:', friend, "\n")
}

R 용어로, 변수 friends는 3개의 문자열을 가지는 벡터이며, for 루프는 벡터내 원소를 하나씩 하나씩 찾아서 벡터에 있는 3개 문자열 각각에 대해 출력을 실행하여 다음 결과를 얻게 된다.

Happy New Year: Joseph 
Happy New Year: Glenn 
Happy New Year: Sally 

for 루프를 영어로 번역하는 것이 while문을 번역하는 것과 같이 직접적이지는 않다. 하지만, 만약 friends를 집합(set)으로 생각한다면 다음과 같다. friends라고 명명된 집합에서 friend 각각에 대해서 한번씩 for 루프 몸통 부문에 있는 문장을 실행하라.

for 루프를 살펴보면, forin은 R 예약어이고 friendfriends는 변수이다.

for(friend in friends) {
  cat('Happy New Year:', friend, "\n")
}

특히, friendfor 루프의 반복 변수(iteration variable)다. 변수 friend는 루프가 매번 반복할 때마다 변하고, 언제 for 루프가 완료되는지 제어한다. 반복 변수는 friends 변수에 저장된 3개 문자열을 순차적으로 훑고 간다.

6.7 루프 패턴

종종 for문과 while문을 사용하여, 벡터나 리스트 항목, 파일 콘텐츠를 훑어 자료에 있는 가장 큰 값이나 작은 값 같은 것을 찾는다.

forwhile 루프는 일반적으로 다음과 같이 구축된다.

  1. 루프가 시작하기 전에 하나 혹은 그 이상의 변수를 초기화
  2. 루프 몸통부분에 각 항목에 대해 연산을 수행하고, 루프 몸통 부분의 변수 상태를 변경
  3. 루프가 완료되면 결과 변수의 상태 확인

루프 패턴의 개념과 작성을 시연하기 위해서 숫자 벡터를 사용한다.

6.7.1 계수와 합산 루프

예를 들어, 벡터의 항목을 세기(counting) 위해서 다음과 같이 for 루프를 작성한다.

count <- 0
for(itervar in c(3, 41, 12, 9, 74, 15)){
  count <- count + 1
}

cat('Count: ', count)
## Count:  6

루프가 시작하기 전에 변수 count를 0 으로 설정하고, 숫자 목록을 훑어 갈 for 루프를 작성한다. 반복(iteration) 변수는 itervar라고 하고, 루프에서 itervar을 사용되지 않지만, itervar는 루프를 제어하고 루프 몸통 부문 리스트의 각 값에 대해서 한번만 실행되게 한다.

루프 몸통 부문에 리스트의 각 값에 대해서 변수 count 값에 1을 더한다. 루프가 실행될 때, count 값은 “지금까지” 살펴본 값의 횟수가 된다.

루프가 종료되면, count 값은 총 항목 숫자가 된다. 총 숫자는 루프 맨마지막에 얻어진다. 루프를 구성해서, 루프가 끝났을 때 기대했던 바를 얻었다.

숫자 집합의 갯수를 세는 또 다른 비슷한 루프는 다음과 같다.

total <- 0
for(itervar in c(3, 41, 12, 9, 74, 15)){
  total <- total + itervar
}

cat('Total: ', total)
## Total:  154

상기 루프에서, 반복 변수(iteration variable)가 사용되었다. 앞선 루프에서처럼 변수 count에 1을 단순히 더하는 대신에, 각 루프가 반복을 수행하는 동안 실제 숫자 (3, 41, 12, 등)를 작업중인 합계에 덧셈을 했다. 변수 total을 생각해보면, total은 “지금까지 값의 총계다.” 루프가 시작하기 전에 total은 어떤 값도 살펴본 적이 없어서 0 이다. 루프가 도는 중에는 total은 작업중인 총계가 된다. 루프의 마지막 단계에서 total은 리스트에 있는 모든 값의 총계가 된다.

루프가 실행됨에 따라, total은 각 요소의 합계로 누적된다. 이 방식으로 사용되는 변수를 누산기(accumulator)라고 한다.

계수(counting) 루프나 합산 루프는 특히 실무에서 유용하지는 않다. 왜냐하면 리스트에서 항목의 개수와 총계를 계산하는 length()sum()가 각각 내장 함수로 있기 때문이다.

6.7.2 최대값과 최소값 루프

리스트, 벡터나 열(sequence)에서 가장 큰 값을 찾기 위해서, 다음과 같이 루프를 작성한다.

largest <- NA

cat('Before:', largest, "\n")

for(itervar in c(3, 41, 12, 9, 74, 15)){
  if(is.na(largest) || itervar > largest){
    largest <- itervar
    cat('Loop:', itervar, largest, "\n")
    cat('Largest:', largest, "\n")
  }
}

프로그램을 실행하면, 출력은 다음과 같다.

Loop: 3 3 
Largest: 3 
Loop: 41 41 
Largest: 41 
Loop: 74 74 
Largest: 74 

변수 largest는 “지금까지 본 가장 큰 수”로 생각할 수 있다. 루프 시작 전에 largest 값은 상수 NA이다. NA은 “빈(empty)” 변수를 표기하기 위해서 변수에 저장하는 특별한 상수 값이다.

루프 시작 전에 지금까지 본 가장 큰 수는 NA이다. 왜냐하면 아직 어떤 값도 보지 않았기 때문이다. 루프가 실행되는 동안에, largest 값이 NA 이면, 첫 번째 본 값이 지금까지 본 가장 큰 값이 된다. 첫번째 반복에서 itervar는 3 이 되는데 largest 값이 NA이여서 즉시, largest값을 3 으로 갱신한다.

첫번째 반복 후에 largest는 더 이상 NA가 아니다. itervar > largest인지를 확인하는 복합 논리 표현식의 두 번째 부분은 “지금까지 본” 값 보다 더 큰 값을 찾게 될 때 자동으로 동작한다. “심지어 더 큰” 값을 찾게 되면 변수 largest에 새로운 값으로 대체한다. largest가 3에서 41, 41에서 74로 변경되어 출력되어 나가는 것을 확인할 수 있다.

루프의 끝에서 모든 값을 훑어서 변수 largest는 리스트의 가장 큰 값을 담고 있다.

최소값을 계산하기 위해서는 코드가 매우 유사하지만 작은 변화가 있다.

smallest <- NA

cat('Before:', smallest, "\n")

for(itervar in c(3, 41, 12, 9, 74, 15)){
  if(is.na(smallest) || itervar < smallest){
    smallest <- itervar
    cat('Loop:', itervar, smallest, "\n")
    cat('Largest:', smallest, "\n")
  }
}

변수 smallest는 루프 실행 전에, 중에, 완료 후에 “지금까지 본 가장 작은” 값이 된다. 루프 실행이 완료되면, smallest는 벡터의 최소값을 담게 된다.

계수(counting)과 합산에서와 마찬가지로 R 내장함수 max()min()은 이런 루프문 작성을 불필요하게 만든다.

다음은 R 내장 min() 함수의 간략 버전이다. getAnywhere(min), .Primitive("min")을 입력해도 원소스코드를 볼 수는 없다. names(methods:::.BasicFunsList) 명령어를 통해 .Primitive() 함수를 파악할 수 있다.

min <- function(values) {
  smallest <- NA
  for(value in values){
    if(is.na(smallest) || value < smallest){
      smallest <- value
  return(smallest)      
  }
}

가장 적은 코드로 작성한 함수 버전은 R에 이미 내장된 min 함수와 동등하게 만들기 위해서 모든 print문을 삭제했다.

6.8 디버깅

좀더 큰 프로그램을 작성할 때, 좀더 많은 시간을 디버깅에 보내는 자신을 발견할 것이다. 좀더 많은 코드는 버그가 숨을 수 있는 좀더 많은 장소와 오류가 발생할 기회가 있다는 것을 의미한다.

디버깅 시간을 줄이는 한 방법은 “이분법에 따라 디버깅(debugging by bisection)” 하는 것이다. 예를 들어, 프로그램에 100 줄이 있고 한번에 하나씩 확인한다면, 100 번 단계가 필요하다.

대신에 문제를 반으로 나눈다. 프로그램 정확히 중간이나, 중간부분에서 점검한다. print문이나, 검증 효과를 갖는 상응하는 대용물을 넣고 프로그램을 실행한다.

중간지점 점검 결과 잘못 되었다면 문제는 양분한 프로그램 앞부분에 틀림없이 있다. 만약 정확하다면, 문제는 프로그램 뒷부분에 있다.

이와 같은 방식으로 점검하게 되면, 검토 해야하는 코드의 줄수를 절반으로 계속 줄일 수 있다. 단계가 100 번 걸리는 것에 비해 6번 단계 후에 이론적으로 1 혹은 2 줄로 문제 코드의 범위를 좁힐 수 있다.

실무에서, “프로그램의 중간”이 무엇인지는 명확하지 않고, 확인하는 것도 가능하지 않다. 프로그램 코드 라인을 세서 정확히 가운데를 찾는 것은 의미가 없다. 대신에 프로그램 오류가 생길 수 있는 곳과 오류를 확인하기 쉬운 장소를 생각하세요. 점검 지점 앞뒤로 버그가 있을 곳과 동일하게 생각하는 곳을 중간지점으로 고르세요.

6.9 용어정의

  • 누산기(accumulator): 더하거나 결과를 누적하기 위해 루프에서 사용되는 변수
  • 계수(counter): 루프에서 어떤 것이 일어나는 횟수를 기록하는데 사용되는 변수. 카운터를 0 으로 초기화하고, 어떤 것의 “횟수”를 셀 때 카운터를 증가시킨다.
  • 감소(decrement): 변수 값을 감소하여 갱신
  • 초기화(initialize): 갱신될 변수의 값을 초기 값으로 대입
  • 증가(increment): 변수 값을 증가시켜 갱신 (통상 1씩)
  • 무한 루프(infinite loop): 종료 조건이 결코 만족되지 않거나 종료 조건이 없는 루프
  • 반복(iteration): 재귀함수 호출이나 루프를 사용하여 명령문을 반복 실행

6.10 연습문제

  1. 사용자가 “done”을 입력할 때까지 반복적으로 숫자를 읽는 프로그램을 작성하세요. “done”이 입력되면, 총계, 갯수, 평균을 출력하세요. 만약 숫자가 아닌 다른 것을 입력하게되면, tryCatch를 사용하여 사용자 실수를 탐지해서 오류 메시지를 출력하고 다음 숫자로 건너 뛰게 하세요.
Enter a number: 4
Enter a number: 5
Enter a number: bad data
Invalid input
Enter a number: 7
Enter a number: done
16 3 5.33333333333
  1. 위에서처럼 숫자 목록을 사용자로부터 입력받는 프로그램을 작성하세요. 평균값 대신에 숫자 목록 최대값과 최소값을 출력하세요.