지난 글에서는 배열을 사용하여 각 숫자의 출현 횟수를 카운트했었는데 오늘은 이러한 경우에 더 적합한 해시(Hash)를 사용해서 프로그램을 작성해보자.
우선 해시에 알아야 하는데 아래 코드 실행 결과를 보면 대강 해시가 뭔지 감이 올 것이다.
>> person = { name: "홍길동", age: 35, job: "Programmer" }
=> {:name=>"홍길동", :age=>35, :job=>"Programmer"}
>> person[:name]
=> "홍길동"
>> person[:age]
=> 35
>> person[:job]
=> "Programmer"
>> person[:height] = 180
=> 180
>> person
=> {:name=>"홍길동", :age=>35, :job=>"Programmer", :height=>180}
>> person[:age] = 37
=> 37
>> person
=> {:name=>"홍길동", :age=>37, :job=>"Programmer", :height=>180}
>> person[:weight]
=> nil
해시는 위 그림과 같이 { 키1: 값1, 키2: 값2, ... } 형태로 쉽게 만들 수 있고 키를 사용하여 대응되는 값을 확인할 수 있다.
또한 해시를 생성한 후에도 새로운 키와 값을 추가할 수도 있고 기존 값을 새로운 값으로 수정도 가능하다.
만약 해시에 없는 키의 값을 조회하면 nil 을 돌려준다는 것도 알아두자.
그리고 :name 처럼 앞에 ':' 문자가 붙어 있는 것을 심볼(Symbol)이라고 하는데 문자열과 비슷하지만 concat 메서드 등을 통해 값을 변경할 수 있는 문자열과 달리 심볼은 값을 변경할 수 없는 특성이 있어 값이 변경되면 안되는 것을 나타낼 때 주로 사용한다. 그래서 보통 해시에서 키로 많이 사용한다.
이제 해시에 대해 간단히 알아봤으니 지난번 프로그램을 아래 코드처럼 수정해 보자.
freqency = {} # 빈 해시 생성
File.foreach("로또당첨번호.txt") do |str|
nums = str.split
for i in 0..5
no = nums[i].to_i
if freqency[no].nil?
freqency[no] = 1
else
freqency[no] += 1
end
end
end
pp freqency.sort_by { |k, v| -v }
테스트를 위해 먼저 앞의 코드를 D:/blog/ruby/lotto 폴더 아래 lotto_freqency_by_hash.rb 파일로 저장하자.
그리고 같은 폴더의 위치에서 cmd 창을 열어 lotto_freqency_by_hash.rb를 실행해 보자.
배열 대신 해시를 사용하면서 숫자와 출현 횟수를 짝짓지 위한 코드가 없어지면서 프로그램이 조금 더 간결해졌다.
그런데 해시에 특정 숫자의 출현 횟수를 저장하려고 할 때 해당 숫자가 아직 해시에 없을 수도 있기 때문에 if / else 조건문이 필요한데 배열에선 필요 없던 코드였다.
이 조건문을 없앨 수는 없을까? 원시적인 방법으로 처음 해시를 만들 때 1부터 45까지를 키로 하고 값을 모두 0으로 해서 만들면 되긴 한다. { 0 => 0, 1 => 0, 2 => 0, ..., 45 => 0 } 이렇게 말이다. 그런데 이보다 더 쉽고 간단한 방법이 있다.
결국 존재하지 않는 키로 해시 값을 얻으려 할 때 nil 대신 특정 값을 받을 수 있으면 되는 건데 해시를 만들 때 리터럴(literal) 방식이 아닌 생성자(constructor)를 사용해서 만들면 된다.
{ 키: 값 } 형태로 해시를 만드는 게 리터럴 방식이고 Hash.new 를 통해 해시를 만드는 것이 생성자를 사용한 방식이다.
생성자(new)도 메서드이기 때문에 인수를 줄 수가 있는데 Hash(해시 클래스)의 생성자에는 인수로 기본값을 줄 수가 있다. 이렇게 기본값을 주고 생성한 해시는 존재하지 않는 키에 대해 값으로 nil이 아니라 그 '기본값'을 돌려주게 된다.
>> h = Hash.new(0)
=> {}
>> h[1]
=> 0
>> h
=> {}
>> h[1] += 1
=> 1
>> h
=> {1=>1}
freqency = Hash.new(0) # 기본값이 0인 빈 해시 생성
File.foreach("로또당첨번호.txt") do |str|
nums = str.split
for i in 0..5
no = nums[i].to_i
freqency[no] += 1
end
end
pp freqency.sort_by { |k, v| -v }
해시의 기본값 설정 기능을 사용하여 if / else 조건문을 제거하니 코드가 한결 더 보기 좋아졌다.
여기서 한 단계만 더 나아가 보자. 이전 글에서 배열의 sort_by 메서드를 호출할 때 함께 건네준 블록이 배열 요소의 수만큼 반복해서 실행되는 것을 보았고 블록 파라미터를 통해 배열 요소의 값을 사용할 수 있다는 것도 알았다.
그렇다면 정렬 등 다른 목적이 아닌 단순히 배열 요소의 수만큼 반복하면서 원하는 작업을 처리하기 위해 사용할 수 있는 메서드도 있지 않을까?
each 메서드가 바로 그러한 기본적인 반복 처리 기능을 제공해준다. 아래의 코드를 보면 for 반복문이 사라지고 each 메서드와 블록이 그 역할을 대신하는 걸 볼 수 있다.
freqency = Hash.new(0) # 기본값이 0인 빈 해시 생성
File.foreach("로또당첨번호.txt") do |str|
nums = str.split
nums.each do |n|
no = n.to_i
freqency[no] += 1
end
end
pp freqency.sort_by { |k, v| -v }
for 반복문에서는 0..5 를 통해 반복문 안의 내용이 몇 번 실행되어야 하는지를 명시적으로 지정하였고 또한 nums 배열의 각 요소 값을 가져오기 위해서도 nums[i] 처럼 작성해야 했다.
그러나 each 메서드를 사용하면 따로 신경쓰지 않아도 호출 대상 배열(여기선 nums)의 크기만큼 반복을 해주고 배열의 각 요소 값도 블록변수에 알아서 담아주므로 그냥 사용하기만 하면 된다.
끝으로 굳이 필요 없는 nums 변수와 no 변수를 제거한 아래 코드로 이번 프로그램을 마무리하도록 하겠다.
freqency = Hash.new(0) # 기본값이 0인 빈 해시 생성
File.foreach("로또당첨번호.txt") do |str|
str.split.each { |n| freqency[n.to_i] += 1 }
end
pp freqency.sort_by { |k, v| -v }
See you again~~