본문 바로가기
Tech/Coding

[Python]collections.Counter

by redcubes 2024. 2. 5.

파이썬 collections.Counter에 대한 설명

파이썬의 collections 모듈에 있는 Counter 클래스는 요소들의 개수를 셀 때 매우 유용한 데이터 구조입니다. Counter는 사실상 사전(dictionary)의 하위 클래스로, 해시 가능한(hashable) 객체를 카운트하는 딕셔너리입니다. 각 요소는 딕셔너리의 키(key)로 저장되며, 그 요소의 개수가 키의 값(value)으로 저장됩니다.

Counter 클래스의 알고리즘

Counter 클래스의 알고리즘은 주어진 입력(리스트, 튜플, 문자열 등)에서 요소의 등장 횟수를 계산하여, 각 요소를 키로, 해당 요소의 개수를 값으로 하는 사전 형태로 저장합니다. 이 과정은 내부적으로 해시 테이블을 사용하여 효율적으로 수행됩니다.

주요 기능

  1. 초기화: Counter 객체는 반복 가능한(iterable) 객체나 다른 매핑(mapping) 객체를 입력으로 받아 초기화될 수 있습니다. 또한, 직접적으로 키워드 인자를 사용하여 초기화하는 것도 가능합니다.
  2. 업데이트: update() 메소드를 사용하여 이미 생성된 Counter 객체에 새로운 요소들을 추가할 수 있습니다.
  3. 연산: Counter 클래스는 두 Counter 객체 간의 덧셈, 뺄셈, 교집합, 합집합 같은 수학적 연산을 지원합니다.
  4. 가장 흔한 요소 찾기: most_common([n]) 메소드는 가장 빈도가 높은 요소들을 반환합니다.

예제 코드

from collections import Counter

fruits = ['apple', 'banana', 'cherry', 'apple', 'cherry', 'cherry']
fruit_counter = Counter(fruits)
print(fruit_counter)  # Counter({'cherry': 3, 'apple': 2, 'banana': 1})

print(fruit_counter.most_common(1))  # [('cherry', 3)]

Counter는 내부적으로 요소들을 효율적으로 카운트하기 위해 해시 테이블을 사용하며, 파이썬의 기본 데이터 구조와 알고리즘을 활용하여 높은 성능을 제공합니다.

라이브러리 내부 작동 알고리즘

파이썬의 collections.Counter 클래스의 내부 작동 알고리즘에 대해 좀 더 자세히 설명하겠습니다. Counter는 내부적으로 파이썬의 딕셔너리(dictionary) 데이터 구조를 기반으로 구현되어 있습니다. 이는 Counter가 해시 테이블을 사용하여 요소의 개수를 효율적으로 카운트한다는 것을 의미합니다.

Counter의 핵심 알고리즘

  1. 데이터 저장 구조: Counter 객체는 내부적으로 해시 테이블을 사용하는 사전(dictionary)을 사용하여 요소(element)와 그 요소의 개수(count)를 키-값(key-value) 쌍으로 저장합니다. 이는 파이썬의 사전 타입과 유사한 메모리 구조를 가집니다.
  2. 초기화 과정: Counter 객체를 초기화할 때, 제공된 반복 가능한(iterable) 객체의 각 요소에 대해, 해시 테이블을 검색하여 해당 요소가 이미 존재하는지 확인합니다.
    • 요소가 존재하면, 그 요소의 카운트를 1 증가시킵니다.
    • 요소가 존재하지 않으면, 새로운 키-값 쌍을 해시 테이블에 추가하고, 그 요소의 카운트를 1로 설정합니다.
  3. update() 메소드: 새로운 요소들이 추가될 때, Counter는 각 요소에 대해 해시 테이블을 업데이트합니다. 이미 존재하는 요소의 경우 카운트가 증가되고, 새 요소는 해시 테이블에 추가됩니다.
  4. most_common() 메소드: 이 메소드는 내부적으로 (요소, 개수) 형태의 튜플 리스트를 생성하고, 개수를 기준으로 튜플 리스트를 내림차순으로 정렬합니다. 정렬된 리스트에서 상위 n개 요소를 선택하여 반환합니다.
  5. 해시 충돌 처리: 파이썬의 딕셔너리 구현과 마찬가지로, Counter는 해시 충돌이 발생할 경우 오픈 어드레싱(open addressing) 또는 체이닝(chaining) 같은 방법을 사용하여 충돌을 해결합니다. 이는 파이썬의 딕셔너리 구현에 따라 다를 수 있으며, 파이썬의 버전에 따라 내부 구현이 달라질 수 있습니다.
  6. 연산자 오버로딩: Counter는 덧셈(+), 뺄셈(-), 교집합(&), 합집합(|) 연산을 지원합니다. 이러한 연산은 내부적으로 두 Counter 객체의 키-값 쌍을 반복하며, 해당 연산에 맞는 새로운 Counter 객체를 생성하여 반환합니다.

내부 최적화

  • 메모리 사용 최적화: Counter 객체는 사용하지 않는 요소의 카운트를 메모리에서 제거하여 메모리 사용을 최적화합니다. 예를 들어, 요소의 카운트가 0이 되면, 해당 요소는 내부 딕셔너리에서 제거될 수 있습니다.
  • 성능 최적화: 파이썬의 해시 테이블 구현은 높은 성능을 위해 최적화되어 있으며, Counter는 이러한 구현을 활용하여 빠른 카운트 연산을 제공합니다.
    Counter의 이러한 내부 작동 방식은 파이썬의 효율적인 데이터 처리를 가능하게 하며, 대규모 데이터에 대한 빈도 수 계산과 같은 작업을 빠르고 효율적으로 수행할 수 있도록 돕습니다.

for 문과 딕셔너리로 구현하면 속도가 같은가? 다르다면 원인이 뭘까?

collections.Counter 클래스를 사용하는 것과 for 문과 딕셔너리를 사용하여 직접 구현하는 것 사이의 속도는 상황에 따라 다를 수 있습니다. 일반적으로, Counter 클래스는 이러한 작업을 위해 최적화되어 있기 때문에, 대부분의 경우에서 더 빠르거나 적어도 비슷한 성능을 제공합니다. 그러나, 구현의 세부 사항과 작업의 복잡성, 그리고 데이터의 크기와 성질에 따라 성능 차이가 발생할 수 있습니다.

Counter 클래스의 최적화

  • 내부 최적화: Counter는 파이썬의 표준 라이브러리의 일부로서, 내부적으로 다양한 최적화가 적용되어 있습니다. 예를 들어, Counter는 요소의 추가와 카운트 업데이트를 매우 효율적으로 처리하기 위해 내부적으로 해시 테이블을 사용합니다.
  • 사용 편의성과 추가 기능: Counter는 빈도 계산 외에도 다양한 유틸리티 메소드를 제공합니다(예: most_common(), 요소 간 연산 등). 이러한 메소드들은 일반적인 딕셔너리를 사용할 때보다 효율적으로 구현되어 있을 수 있습니다.

직접 구현 시 고려할 점

  • 코드 최적화: 직접 for 문과 딕셔너리를 사용하여 구현할 때, 코드의 효율성은 구현 방식에 크게 의존합니다. 예를 들어, if 문을 사용하여 딕셔너리에 키가 존재하는지 매번 확인하는 대신 dict.get() 또는 dict.setdefault() 메소드를 사용하면 성능을 개선할 수 있습니다.
  • 조건 분기의 오버헤드: 직접 구현 시, 특정 요소가 딕셔너리에 이미 존재하는지 확인하고 적절히 카운트를 업데이트하는 과정에는 추가적인 조건 분기가 필요합니다. 이러한 조건 분기는 실행 시간에 영향을 줄 수 있으며, Counter의 내부 구현이 이를 더 효율적으로 처리할 가능성이 있습니다.

성능 차이의 원인

  • 내부 구현 차이: Counter와 직접 구현 사이의 성능 차이는 대부분 내부 구현의 효율성 차이에서 비롯됩니다. Counter는 파이썬의 내부 최적화와 CPython 레벨에서의 구현을 활용할 수 있어, 순수 파이썬 코드로 구현된 로직보다 성능이 우수할 수 있습니다.
  • 함수 호출 오버헤드: 직접 구현 시, 추가적인 함수 호출이 필요할 수 있으며, 이는 성능에 부정적인 영향을 미칠 수 있습니다. 반면, Counter의 메소드들은 이러한 오버헤드를 최소화하기 위해 최적화될 수 있습니다.

결론적으로, Counter 클래스는 일반적으로 대부분의 사용 사례에서 좋은 성능을 제공합니다. 그러나 매우 특정적인 요구 사항이나 최적화가 필요한 경우, 직접 구현을 통해 성능을 조금 더 끌어올릴 수도 있습니다. 실제 성능 차이를 확인하기 위해서는 특정 작업에 대한 벤치마크를 수행하는 것이 좋습니다.