루비 튜토리얼 4편을 쓰고 나서 3년이 지났습니다. 음음. 그간 많이 게을렀군요. 변명을 해보자면 결혼(!)이라던지 하는 여러가지 개인적 사정이 있었답니다. 기다려주신 분이 있다면 죄송합니다.
그럼 5편을 바로 시작해볼까요.
지금까지는 재미없지만 몰라서는 안될 기본적인 사항들을 다뤘습니다. 기본적인 화면출력, 자료구조, 분기문과 반복문, 클래스 같은 것을 다루었죠. 이제 조금 생소하지만 정말 재밌는 부분으로 넘어가 봅시다. 여기부턴 다른 언어에서는 만나보기 힘든 내용이니 조심조심 따라와 주세요.
오늘은 파일을 열어볼 겁니다. 파일 읽고 쓰기는 참 쉽습니다.
자 이제 메모장이나 여러분이 사랑하는 어떤 텍스트 에디터로든지 파일을 열어보면 세 사람의 이름이 한줄씩 써 있을 겁니다.
이게 어디가 다른 언어에서 만나보기 힘든 내용이냐구요? 에이... 조금만 기다려 보세요.
파일을 읽기도 해봐야죠.
쉽죠? 그렇지만 파일 전체를 읽어서 메모리에 저장하는 건 큰 파일을 다뤄야 할 때는 현명하지 못한 방법이죠. 파일을 한줄한줄 읽어봅시다.
그럴싸하죠?
이 코드에서
루비에는 배열에서 사용한
재미있죠? 블록을 받아 동작하는 다양한 함수들이 있기 때문에 루비 코드는 간결하고 아름다워집니다. 서로 비슷한 일을 하지만 아주 조금씩 다른 동작 때문에 비슷비슷한 루프를 만들어야 했던 어떤 언어들과 비교하면 참으로 상쾌하죠.
블록을 받는 함수는 우리도 만들 수 있습니다. 조금 전까지 열었다 닫았다 했던 그 파일 기억하시죠? 파일을 열고 닫는 코드를 반복해서 썼었죠. 솔직히 그런 반복은 지겹습니다. 우리는 게으르잖아요. 파일을 필요할 때 열고 할일이 끝나면 알아서 닫게 하고 싶으면 어떻게 하면 될까요?
대체 무슨 일이 일어난 건가요?
이 코드는 한줄한줄 따라가면서 설명해 보겠습니다.
먼저 test_open 함수를 정의했죠. 함수 안에는 처음보는 키워드 yield가 포함되어 있군요. 안에 yield 문이 들어있는 함수는 "아 이 함수는 블록을 붙여서 사용하는 함수구나" 하고 생각하시면 됩니다.
test_open 함수를 블록과 함께 호출하면 일단 블록은 무시하고 먼저 함수가 실행됩니다. 함수의 첫번째 줄로 가봅시다.
파일을 열어서 myfile 변수에 담았죠
yield는 함수에 주어진 블록을 실행하는 부분입니다. yield 뒤에 인자를 주면 해당 인자가 블록에 전달되지요. 우리도 블록으로 따라가 볼까요.
해당 블록은 do ... end 부분이고 |f| 를 인자로 받는 것을 알 수 있죠. 아까 yield myfile로 이 블록을 호출했으니 인자 f 에는 아까 저장된 myfile의 값이 저장됩니다.
이곳에서 f의 내용이 한줄씩 화면에 출력되고 블록은 종료됩니다.
블록의 내용이 종료되면 context는 다시 원래 함수의 원래 위치로 돌아가서 실행을 계속합니다. yield의 다음줄은 파일 닫는 구문이었죠. 논리 부정 연산자 not 도 봐둡시다.
파일을 안전하게 닫고 함수는 정상적으로 종료됩니다.
어떻습니까? 파일 닫기를 내가 굳이 부르지 않아도 마지막에 자동으로 호출해주는 안전한 함수가 쉽게 완성되었죠. 이 함수는 사실 루비가 기본으로 제공하는 함수를 직접 손으로 구현해본 것입니다. open 함수는 블록과 함께 부르면 정확히 우리의 test_open 함수처럼 동작합니다. 정말 그런지 확인해볼까요?
재미있으셨나요? 머리가 아프신가요? 아직 이정도에 좌절하시면 안됩니다. 오늘 내용을 정리할 겸, 제가 다음 튜토리얼을 쓸 때까지 시간벌기를 할 겸 (...), 연습문제를 하나 드리죠. 1편부터 5편까지의 내용을 이용해서 my_times 함수를 구현해 보세요. my_times 함수는 다음과 같이 동작해야 합니다.
어떻게 하면 블록을 원하는 숫자만큼 실행하게 할 수 있을까요?
힌트 : yield 키워드는 다른 루비 함수처럼 인자 없이도 사용할 수 있습니다.
연습문제 답은 밑에 접어두겠습니다.
그럼 5편을 바로 시작해볼까요.
지금까지는 재미없지만 몰라서는 안될 기본적인 사항들을 다뤘습니다. 기본적인 화면출력, 자료구조, 분기문과 반복문, 클래스 같은 것을 다루었죠. 이제 조금 생소하지만 정말 재밌는 부분으로 넘어가 봅시다. 여기부턴 다른 언어에서는 만나보기 힘든 내용이니 조심조심 따라와 주세요.
오늘은 파일을 열어볼 겁니다. 파일 읽고 쓰기는 참 쉽습니다.
# 파일은 open 함수를 이용해서 열 수 있습니다.
# "test.txt" 파일을 쓰기 모드("w")로 열어봅시다.
# 주의! 윈도우에서는 "wb" 모드로 여는 것을 추천합니다.
f = open("test.txt", "w")
# open 명령으로 파일 오브젝트를 열어 f 라는 변수에 넣었습니다.
# 이제 1편에서 보았던 puts 함수를 쓸 수 있습니다.
f.puts "Matz"
# print 함수는 뒤에 newline이 붙지 않는 것 기억하시죠?
# 윈도우에서는 \n 대신 \r\n 이 newline입니다.
f.print "Steve Jobs\n"
# write 함수도 있습니다. print와 거의 같습니다.
# 마찬가지로 윈도우에서는 \n 대신 \r\n 을 써줍시다.
f.write "Bill Gates\n"
# 아무리 게을러도 다 쓴 파일은 반드시 닫아야 훌륭한 프로그래머가 됩니다.
f.close
자 이제 메모장이나 여러분이 사랑하는 어떤 텍스트 에디터로든지 파일을 열어보면 세 사람의 이름이 한줄씩 써 있을 겁니다.
이게 어디가 다른 언어에서 만나보기 힘든 내용이냐구요? 에이... 조금만 기다려 보세요.
파일을 읽기도 해봐야죠.
# 아까 그 파일을 읽기 모드 "r"로 읽습니다.
# 주의! 쓸 때와 마찬가지로 윈도우에서는 "rb" 모드로 여는 것을 추천합니다.
f = open("test.txt", "r")
# read 함수는 파일 전체를 읽어서 문자열로 저장합니다.
string = f.read
puts string
# 파일을 썼으면 닫아야죠.
f.close
Matz
Steve Jobs
Bill Gates
쉽죠? 그렇지만 파일 전체를 읽어서 메모리에 저장하는 건 큰 파일을 다뤄야 할 때는 현명하지 못한 방법이죠. 파일을 한줄한줄 읽어봅시다.
f = open("test.txt", "r")
# 2편에서 봤던 each 함수 기억하세요?
# 파일 오브젝트에는 each_line 함수가 있습니다!
# 파일을 한줄씩 읽어서 line 변수에 저장하고, line 변수를 print 함수로 출력합니다.
f.each_line { |line| print line }
f.close
Matz
Steve Jobs
Bill Gates
그럴싸하죠?
{ |line| print line } 요 부분이 오늘의 주인공, 블록(block)입니다. 블록은 인자를 받을 수 있는 임시 코드 조각으로 생각하시면 됩니다. 여기서는 |line| 이 인자가 되고 print line 이 코드 조각이 되는 것이죠.
f.each_line { |line| print line }
이 코드에서
f.each_line 함수는 파일을 한줄한줄 읽어들인 후 line 인자에 넣어 블록으로 전달합니다. 우리가 아까 쓴 파일에는 3개의 이름이 있으니, 블록은 3번 실행되겠죠.루비에는 배열에서 사용한
each 함수나 파일에서 사용한 each_line 함수처럼 코드 블록을 인자처럼 사용할 수 있는 여러가지 함수들이 있습니다. 재미있는 것만 몇가지 살펴볼까요?
# 모든 정수에는 times 함수가 존재합니다.
# 블록을 해당 숫자만큼 반복 실행하는 함수입니다.
3.times { puts "knock" }
knock
knock
knock
# There is more than one way to do it! 기억하시나요?
# { ... } 블록 대신 do ... end 로 감싸서 사용할 수도 있습니다.
# 의미는 완전히 동일합니다.
3.times do
puts "knock"
end
knock
knock
knock
# 문자열을 한글자씩 잘라주는 each_char 함수도 유용합니다.
"ABC".each_char do |c|
puts c
end
A
B
C
# 1편에서 봤던 자료구조 Hash 기억하시죠?
h = {"one" => 1, "two" => 2, "three" => 3}
# Hash에도 each 함수가 있습니다.
# Hash의 each 함수는 key와 value를 쌍으로 블록에 넘깁니다.
h.each do |key, value|
puts key
puts value
end
one
1
two
2
three
3
# map 함수는 배열의 모든 값에 블록 내용을 실행한 새로운 배열을 리턴합니다.
# 배열의 값에 2를 곱해 볼까요?
[1, 2, 3, 4].map {|x| x * 2}
# => [2, 4, 6, 8]
# 특정 조건을 만족하는 값을 찾는 것도 select 함수로 간단히 해결됩니다.
# 1, 2, 3, 4 가운데 짝수만을 리턴하게 해봅시다.
[1, 2, 3, 4].select { |x| x % 2 == 0 }
# => [2, 4]
# 정수 오브젝트에 포함된 even? 함수를 이용하면 더욱 간단합니다.
# 함수 뒤의 물음표도 함수 이름에 포함되므로 붙여써야 하는 점에 주의하세요.
[1, 2, 3, 4].select { |x| x.even? }
# => [2, 4]
재미있죠? 블록을 받아 동작하는 다양한 함수들이 있기 때문에 루비 코드는 간결하고 아름다워집니다. 서로 비슷한 일을 하지만 아주 조금씩 다른 동작 때문에 비슷비슷한 루프를 만들어야 했던 어떤 언어들과 비교하면 참으로 상쾌하죠.
블록을 받는 함수는 우리도 만들 수 있습니다. 조금 전까지 열었다 닫았다 했던 그 파일 기억하시죠? 파일을 열고 닫는 코드를 반복해서 썼었죠. 솔직히 그런 반복은 지겹습니다. 우리는 게으르잖아요. 파일을 필요할 때 열고 할일이 끝나면 알아서 닫게 하고 싶으면 어떻게 하면 될까요?
def test_open
# 파일 열고 닫기가 지겨워서 함수에 담아보려고 합니다.
myfile = open("test.txt", "r")
# 이 한줄이 마술의 핵심입니다.
yield myfile
# 파일을 닫습니다.
# 이미 닫힌 파일을 두번 닫으면 에러가 일어나므로
# 닫혀 있는지 먼저 체크해야 합니다.
if not myfile.closed?
myfile.close
end
end
test_open do |f|
f.each_line do |line|
print line
end
end
Matz
Steve Jobs
Bill Gates
대체 무슨 일이 일어난 건가요?
이 코드는 한줄한줄 따라가면서 설명해 보겠습니다.
먼저 test_open 함수를 정의했죠. 함수 안에는 처음보는 키워드 yield가 포함되어 있군요. 안에 yield 문이 들어있는 함수는 "아 이 함수는 블록을 붙여서 사용하는 함수구나" 하고 생각하시면 됩니다.
test_open do |f|
...
end
test_open 함수를 블록과 함께 호출하면 일단 블록은 무시하고 먼저 함수가 실행됩니다. 함수의 첫번째 줄로 가봅시다.
myfile = open("test.txt", "r")
파일을 열어서 myfile 변수에 담았죠
yield myfile
yield는 함수에 주어진 블록을 실행하는 부분입니다. yield 뒤에 인자를 주면 해당 인자가 블록에 전달되지요. 우리도 블록으로 따라가 볼까요.
test_open do |f|
...
end
해당 블록은 do ... end 부분이고 |f| 를 인자로 받는 것을 알 수 있죠. 아까 yield myfile로 이 블록을 호출했으니 인자 f 에는 아까 저장된 myfile의 값이 저장됩니다.
f.each_line do |line|
print line
end
이곳에서 f의 내용이 한줄씩 화면에 출력되고 블록은 종료됩니다.
블록의 내용이 종료되면 context는 다시 원래 함수의 원래 위치로 돌아가서 실행을 계속합니다. yield의 다음줄은 파일 닫는 구문이었죠. 논리 부정 연산자 not 도 봐둡시다.
if not myfile.closed?
myfile.close
end
파일을 안전하게 닫고 함수는 정상적으로 종료됩니다.
어떻습니까? 파일 닫기를 내가 굳이 부르지 않아도 마지막에 자동으로 호출해주는 안전한 함수가 쉽게 완성되었죠. 이 함수는 사실 루비가 기본으로 제공하는 함수를 직접 손으로 구현해본 것입니다. open 함수는 블록과 함께 부르면 정확히 우리의 test_open 함수처럼 동작합니다. 정말 그런지 확인해볼까요?
# nil 은 빈 값을 의미합니다. Java의 null이나 C/C++ 의 NULL 에 해당합니다.
# 여기서는 비어있는 변수 myfile을 만들어 봅시다.
myfile = nil
open("test.txt", "r") do |f|
# 블록 인자는 블록 바깥에서 접근할 수가 없습니다.
# 여기선 나중에 열어보기 위해 외부 변수인 myfile에 담아봅시다.
myfile = f
# 파일 내용을 출력한 후 닫습니다.
puts f.read
end
# 블록과 함께 호출했기 때문에, open 함수의 실행이 끝나면 파일은 이미 닫혀 있습니다.
# myfile.read는 파일이 이미 닫혀 있다는 내용의 에러를 일으킵니다.
myfile.read
Matz
Steve Jobs
Bill Gates
tutorial5-3.rb:16:in `read': closed stream (IOError)
from tutorial5-3.rb:16:in `<main>'
재미있으셨나요? 머리가 아프신가요? 아직 이정도에 좌절하시면 안됩니다. 오늘 내용을 정리할 겸, 제가 다음 튜토리얼을 쓸 때까지 시간벌기를 할 겸 (...), 연습문제를 하나 드리죠. 1편부터 5편까지의 내용을 이용해서 my_times 함수를 구현해 보세요. my_times 함수는 다음과 같이 동작해야 합니다.
my_times(3) do
puts "knock"
end
knock
knock
knock
어떻게 하면 블록을 원하는 숫자만큼 실행하게 할 수 있을까요?
힌트 : yield 키워드는 다른 루비 함수처럼 인자 없이도 사용할 수 있습니다.
연습문제 답은 밑에 접어두겠습니다.
답 확인해보기
'루비 튜토리얼' 카테고리의 다른 글
| 게으른 프로그래머를 위한 루비 튜토리얼 #5 (3) | 2011/09/15 |
|---|---|
| 게으른 프로그래머를 위한 루비 튜토리얼 #4 (1) | 2008/10/22 |
| 게으른 프로그래머를 위한 루비 튜토리얼 #3 (0) | 2008/01/12 |
| 게으른 프로그래머를 위한 루비 튜토리얼 #2 (0) | 2007/09/04 |
| 게으른 프로그래머를 위한 루비 튜토리얼 #1 (18) | 2007/03/31 |
