--- title: Counter | Tips in Python description: How to use counter/defualtdict/heapq in Python. tags: python-tips, tips, python, programming, coding, defaultdict, counters, collections GA: UA-206161787-1 lang: zh --- # [EP0] 計數 | Python小技巧 Python從1991年發表到現在也已經三十年了。在這三十年的過程中,語言本身也隨著使用者的需求而成長。身為碼農的我們,也需要與時俱進。這邊我們透過一個常見的例子,來看看這三十年來,在Python中,對於同一個問題的解法,是如何演化的吧! ## 在一個陣列中,尋找出現次數最多的前三名元素。 這種統計次數的問題在生活中非常實用,就拿台灣最喜歡的選舉來說,今天我們要統計選票,在不考慮廢票的情況下,每一張票都有一個指定的候選人。那出來的資料可能是像下面這樣:(這邊就以`P0`到`P9`來代表十組候選人的名字) ```python= # Input candidates = ["P1", "P2", "P1", "P5", "P3", ...] ``` 我們最後希望能找到得票數最高的前三名候選人,列出他們的`名字`以及`得票數`。 ```python= # Output winners = [("P3", 556), ("P1", 318), ("P7", 242)] ``` ### 方法一:1991年的直觀解法 ```python= def top3(candidates: List) -> List[Tuple[str, int]]: counts = dict() for c in candidates: if c not in counts: counts[c] = 1 else: counts[c] += 1 result = [(name, count) for name, count in counts.items()] # Return the second element in the tuple for sorting. cmp_fn = lambda t: t[1] result.sort(key=cmp_fn, reverse=True) return result[:3] ``` 最簡單直觀的想法,我們就一個一個數,最後得到十組候選人的票數,最後再依照得票數排序,選出前三名。即使是古老的Python,整體的程式碼還不算太糟糕,用到`Dictionary`, `List comprehension`, `sort(key=,)`的排列函式,以及`[:3]`的語法來取出排序後的前三個元素。 ### 方法二:Python 2.3 (2003)的改進 為了找到前三名,我們就對整個序列的十名候選人去做排序(`O(NLogN)`),顯然是有點浪費。這種取前N名的問題,我們其實可以利用資料結構裡的`Min Heap`來加速(`O(N)`),在這邊先不討論`Min Heap`的細節,我們只要知道他是一種容器,每當在加入物件時,能維持整體的大小排序。 在Python 2.3中,加入了`heapq`這個模組到內建的函式庫裡,讓我們可以很簡單的來應用這個資料結構。 ```python= import heapq def top3_heapq(candidates): # Same counts = dict() for c in candidates: if c not in counts: counts[c] = 1 else: counts[c] += 1 # --- New --- result = heapq.nlargest(3, [(counts[name], name) for name in counts]) return [(name, count) for count, name in result] ``` ### 方法三:Python 2.5 (2005)的`defaultdict` 我們改進了後半段的程式,那麼前半段在統計各個候選人的得票部分呢?大家有沒有發現,在統計每張票時,我們都得檢查該候選人的名字有沒有在`dictionary`裡面了,如果沒有,我們得先在`dictionary`中為他建立一個`key`並且給他第一張票。 ```python= # Same counts = dict() for c in candidates: if c not in counts: counts[c] = 1 else: counts[c] += 1 ``` 在Python2.5中,引入了`collections`的模組,其中的`defaultdict`是我們今天的主角。每當使用`defaultdict`建立`dictionary`時,我們可以傳入一個工廠函式(factory function),之後當我們存取某個不存在的`key`時,他會自動呼叫這個工廠函式來為這個`key`建立一個預設的值。 ```python= import collections import heapq def top3_defaultdict(candidates): # --- New --- # python中,呼叫內建的int函式而不提供任何參數,會返回0。 # int() == 0 counts = collections.defaultdict(int) for c in candidates: counts[c] += 1 # Same result = heapq.nlargest(3, [(counts[name], name) for name in counts]) return [(name, count) for count, name in result] ``` ### 方法三:Python 2.7 (2007)的`Counter` 由於計數這件事實在是太常見了,在2007的Python2.7中,加入了`Counter`到`collections`的內建模組下,我們就不用再關心`heapq`到底該如何使用,我們直接透過簡單的幾行程式碼就可以完成一樣的工作了!(甚至有更好的效能) ```python= def top3_counter(candidates): counts = collections.Counter(candidates) return counts.most_common(3) ``` ## 效能 新的方法不只有可讀性更高的程式碼,在效能上也是有大大的提升。像這邊使用`Counter`比起我們最一開始的方法快了兩倍。 ``` naive_time: 31.5s heapq_time: 35.3s defaultdict_time: 27.1s counter_time: 16.0s ``` ## 後記 - 程式碼範例: https://colab.research.google.com/drive/1ToHXKGUjUyiCs_IEzZkPsHEYT3W3T_b3?usp=sharing