2023. 2. 13. 21:00ㆍIT/Concept

String 분석 시 유용하게 사용가능한 nanoCounter; Python의 collections.Counter를 Golang으로 구현해보자.
1. Golang과 Python의 차이점
Golang은 단순한 언어다. Golang에는 while문도 없고 상속도 없다. Golang에는 개발자가 편리하게 사용가능한 built-in 함수가 거의 없다. 반면 진정한 의미에서 객체지향 언어라 할 수 있는 Python이나 Dart와 같은 언어에서는, ‘이런 기능 있을 것 같은데’ 생각을 하는 순간 보통 있음을 바로 확인할 수 있다.
2. Python의 collections.Counter
문자열 분석 시, 우리는 종종 각 character가 몇 번씩 출현하는지, 가장 많이 등장하는 character가 무엇인지 알아야하는 경우가 있다. 대부분의 경우 map을 사용해 이를 어렵지 않게 해결할 수 있다. 이때 Python의 경우, dictionary로 코드를 구현할 필요 없이 collections 모듈의 Counter를 사용하면 한 줄의 코드로 원하는 기능을 구현가능하다.
counter = collections.Counter("Hello world")
# Counter({'l': 3, 'o': 2, 'H': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1, '!': 1})
Python의 Counter는 List, Dictionary, String 등 다양한 타입의 Input을 받을 수 있다. 이렇게 input을 받아 각 요소를 key, 빈도수를 value로 하는 정렬된 Dictionary를 output으로 return한다. 나아가 Counter내부는 update(), elements(), most_common(n) 등의 여러 유용한 method가 존재한다.
그러나 우리의 nanoCounter는 String만을 parameter로 받을 것이다. 또한 Counter 내 모든 기능을 포함하지 않고 가장 유용하게 자주 쓰이는 most_common(n) 기능만을 지니고 있을 것이다. 게다가 Python과는 다르게 Golang에서는 map이 순서를 보장하지 않는다. 따라서 nanoCounter 함수는 다음과 같이 문자열과 몇번째까지 가장 큰 숫자를 받을지 (most_common(n)의 n) 을 input으로 받으면 글자와 빈도수 struct를 원소로 갖는 리스트를 map 대신 출력한다. 리스트 대신 Heap의 사용 또한 가능하겠으나, 여기서는 우선 처음 Golang을 접하는 사람도 최대한 직관적으로 이해가 가능하도록 코드를 쉽게 작성했다.
| nanoCounter() | |
| input | input (string), n (int) / eg. (ababa, 1) |
| output | res (list) / eg. [{a 3}] |
3. 구현
우선 nanoCounter()가 출력할 리스트의 원소들은 다음과 같은 struct다.
type element struct {
char string
occurrence int
}
이제 본격적으로 nanoCounter()를 구현해보자.
가장 먼저 counter map을 만든다. 이 map은 주어지는 문자열 내 서로 다른 글자가 몇 개인지, 각각의 글자는 몇 번 등장하는지 알 수 있게 해준다.
counter := make(map[string]int)
// create counter
for _, c := range input {
counter[string(c)]++
}
다음으로는 {글자, 등장 횟수}를 담은 list를 만든다.
Python의 dictioinary와 달리 Golang의 map은 순서를 보장하지 않는다. 때문에 안타깝게도 위에서 만든 map만을 사용해 온전히 기능을 구현하고 싶지만 그렇게 하기는 힘들다. 따라서 우리는 별도의 list가 필요하다.
// create struct list
chars := make([]element, 0, len(counter))
for chr := range counter {
ele := element{}
ele.char = chr
ele.occurrence = counter[chr]
chars = append(chars, ele)
}
해당 리스트를 정렬한다.
// sort descending order
sort.Slice(chars, func(i, j int) bool {
return chars[i].occurrence > chars[j].occurrence
})
이제 함수의 return값을 정의한다.
여기서 n은 가변인자로 아무런 값을 입력하지 않았을 경우 nil로 받을 수 있도록 nanoCounter의 parameter에 약간의 트릭(n ...int)을 사용했다.
nil이란 zero value는 명시적인 초기값을 할당하지 않고 변수를 만들었을 때 해당 변수가 갖게 되는 값으로, 포인터, 인터페이스, 맵, 슬라이스, 채널, 함수 등의 zero value이 될 수 있다.
// get the result
if n == nil || n[0] > len(counter) || n[0] <= 0 {
return chars
} else {
res := chars[0:n[0]]
return res
}
전체 함수는 다음과 같다.
type element struct {
char string
occurrence int
}
func nanoCounter(input string, n ...int) []element {
counter := make(map[string]int)
// create counter
for _, c := range input {
counter[string(c)]++
}
// create struct list
chars := make([]element, 0, len(counter))
for chr := range counter {
ele := element{}
ele.char = chr
ele.occurrence = counter[chr]
chars = append(chars, ele)
}
// sort descending order
sort.Slice(chars, func(i, j int) bool {
return chars[i].occurrence > chars[j].occurrence
})
// get the result
if n == nil || n[0] > len(counter) || n[0] <= 0 {
return chars
} else {
res := chars[0:n[0]]
return res
}
}
이제 "Hello World" 문자열을 input으로 테스트를 진행해보자.
package main
import (
"fmt"
"sort"
)
func main() {
var input = "Hello World"
counter := nanoCounter(input, 1)
fmt.Println("if n == 1 -> the most common one: ", counter)
counter = nanoCounter(input, 3)
fmt.Println("if n == 3 -> three most common ones: ", counter)
counter = nanoCounter(input, 0)
fmt.Println("if n == 0 -> whole occurrence: ", counter)
counter = nanoCounter(input, 50)
fmt.Println("if n == 50 -> whole occurrence: ", counter)
counter = nanoCounter(input)
fmt.Println("if there's no n -> whole occurrence: ", counter)
}
type element struct {
char string
occurrence int
}
func nanoCounter(input string, n ...int) []element {
counter := make(map[string]int)
// create counter
for _, c := range input {
counter[string(c)]++
}
// create struct list
chars := make([]element, 0, len(counter))
for chr := range counter {
ele := element{}
ele.char = chr
ele.occurrence = counter[chr]
chars = append(chars, ele)
}
// sort descending order
sort.Slice(chars, func(i, j int) bool {
return chars[i].occurrence > chars[j].occurrence
})
// get the result
if n == nil || n[0] > len(counter) || n[0] <= 0 {
return chars
} else {
res := chars[0:n[0]]
return res
}
}
테스트 결과는 다음과 같다.
// if n == 1 -> the most common one: [{l 3}]
// if n == 3 -> three most common ones: [{l 3} {o 2} {r 1}]
// if n == 0 -> whole occurrence: [{l 3} {o 2} {d 1} {H 1} {e 1} { 1} {W 1} {r 1}]
// if n == 50 -> whole occurrence: [{l 3} {o 2} { 1} {W 1} {r 1} {d 1} {H 1} {e 1}]
// if there's no n -> whole occurrence: [{l 3} {o 2} { 1} {W 1} {r 1} {d 1} {H 1} {e 1}]
더 자세히 알아보기
관련서적: What does nil mean in golang?
-학습을 진행하며 남기는 지식 포스팅-
-부족한 설명이 있다면 부디 조언 부탁드립니다-
이 포스팅은 쿠팡 파트너스 활동의 일환으로,
이에 따른 일정액의 수수료를 제공받습니다
'IT > Concept' 카테고리의 다른 글
| Dart의 Compile Platform (46) | 2023.08.03 |
|---|---|
| Dart의 var, final, const (53) | 2023.07.27 |
| 자주 쓰는 Git Command를 정리해보자 (0) | 2022.08.25 |
| Javascript의 클로저란? (0) | 2022.08.11 |
| Javascript의 프로토타입이란? (0) | 2022.08.08 |