Golang으로 Python의 collections.Counter 구현하기

2023. 2. 13. 21:00IT/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