Python 解決問題的方式經常會隨著時間的推移而發生改變。隨著Python的演變,Python列表的計數方式也得到了發展。
對列表中各專案進行計數有各種各樣的方法,接下來我們將通過觀察每一種方法的程式碼風格,來分析這些技術的不同之處。至於它們的效能,我們以後再考慮。
我們需要了解一些 Python 的歷史來理解這些不同的方法。幸運的是,我們可以用 Python 的 __future__ 模組,坐上時光機。現在讓我們坐上德羅瑞恩(注:即系列電影《回到未來》中的時間機器),駛向1997年。
if 語句
現在是 1997 年 1 月,我們使用的是 Python 1.4。有一個顏色列表,我們很想要知道每一種顏色分別出現了多少次。我們來使用字典看看。
1 2 3 4 5 6 7 |
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = {} for c in colors: if color_counts.has_key(c): color_counts[c] = color_counts[c] + 1 else: color_counts[c] = 1 |
注:我們沒有用+=,因為增量賦值語句在 Python2.0 版之後才被新增進去。我們也沒有使用 c in color_counts 語句,因為這個語句到 Python2.2 版本才可以使用。
執行上述程式碼後,我們可以看到 color_counts 字典裡包含了每一種顏色出現的次數。
1 2 |
>>> color_counts {'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1} |
這段程式碼很簡單。通過迴圈遍歷每一種顏色,檢查每種顏色是否已經出現在字典中,如果還沒有,把它新增到字典中。如果已經出現,那麼把它的數量增一。
我們也可以按照下面的方式來寫:
1 2 3 4 5 6 |
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = {} for c in colors: if not color_counts.has_key(c): color_counts[c] = 0 color_counts[c] = color_counts[c] + 1 |
對於稀疏列表(沒有很多重複顏色的列表),這種方式也許會有一點點慢,因為在 for 迴圈裡需要執行兩條語句。但是我們不擔心它的效能,我們考慮的是它的程式碼風格。深思熟慮之後,我們決定使用新版本的程式碼。
試試塊語句
現在是 1997 年 1 月 2 日,我們仍然在使用 Python 1.4。某天的早晨,我們醒來後突然意識到,我們本來應該按照“先斬後奏” 的方式去實踐(這種方式更符合 Python 的思想),但實際上,我們是按照“三思而後行”的想法在編寫程式碼。現在按照“先斬後奏”的方式,把我們的程式碼重寫成一個 try-except 的塊語句:
1 2 3 4 5 6 7 |
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = {} for c in colors: try: color_counts[c] = color_counts[c] + 1 except KeyError: color_counts[c] = 1 |
這段程式碼試著為每一種顏色的數量增一,但是如果這種顏色還沒有出現在字典中,就會產生 KeyError 錯誤,那麼該種顏色的數量就被初始化為 1。
get 方法
現在是 1998 年 1 月 1 日,Python 已經升級到 1.5 版本。我們決定用字典中的 get 方法來重寫程式碼。
1 2 3 4 |
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = {} for c in colors: color_counts[c] = color_counts.get(c, 0) + 1 |
這段程式碼迴圈遍歷每一種顏色,每次迴圈從字典中獲得當前顏色的計數,預設計數為零,然後計數加一,最後把新值賦給這個字典的鍵,也就是顏色。
這段程式碼很棒的是 for 迴圈裡只有一行語句,但是我們並不確定這是不是更加符合 Python 的風格。我們覺得也許這次的改進顯得太智慧了,所以我們決定將這次的改進還原。
setdefault 方法
現在是 2001 年 1 月,我們已經在使用 Python 2.0 版。在 Python 2.0 版中,字典裡新新增了 setdefault 的方法。於是我們決定用 setdefault 的方法和 += 的增量運算子來重寫程式碼:
1 2 3 4 5 |
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = {} for c in colors: color_counts.setdefault(c, 0) color_counts[c] += 1 |
在每次迴圈裡,不管 setdefault 的方法是否會被用到都會被呼叫一次,但是這次的更改可讀性更強,而且比之前的方法更符合Python的程式碼風格,因此我們決定保留這次更改。
fromkeys方法
現在是 2004 年 1 月 1 日,我們在使用 Python 2.3。聽說字典裡新增了一種新的 fromkeys 方法,這種方法可以從序列中獲取鍵值來建立字典。我們用這個新方法來重寫程式碼:
1 2 3 4 |
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = dict.fromkeys(colors, 0) for c in colors: color_counts[c] += 1 |
這段程式碼用顏色作為鍵建立了一個新的字典,並且每種顏色的數量被初始化為0。 這種方式可以直接增加每一種顏色的數量,不需要擔心這種顏色有沒有被包含到字典中。這段程式碼沒有檢查和例外情況的處理,我們認為這是一次改進,因此我們保留這次更改。
集合
現在是 2005 年 1 月 1 日,我們在使用 Python2.4。我們意識到現在可以使用集合(在 Python 2.3 版時釋出,內建在 Python 2.4 版)和連結串列(在 Python 2.0 版時釋出)來解決計數問題。再思考之後,我們突然想起生成器表示式也只是在 Python 2.4 時才公佈,因此我們決定使用生成器表示式中的一種來進行計數。
1 2 |
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = dict((c, colors.count(c)) for c in set(colors)) |
注:我們沒有使用字典解析,因為字典解析到 Python2.7 版才可以使用。
這個方法不錯,只用了一行程式碼。但是這符合 Python 的風格嗎?
我們記得在《Python 之禪》中,從 python 列表的郵件執行緒開始介紹,到 Python 發展到 2.2.1 版時結束。在我們的互動式直譯器中輸入 import this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
>>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those! |
這段程式碼的複雜度是 O(n2),與複雜度為 O(n) 的程式碼相比,這段程式碼的美觀度和可讀性更差。這次的改變只是一次很有趣的實驗,但是這種一行式的方法更不符合 Python 的風格。因此我們決定將這次改變恢復原狀。
defaultdict 方法
現在是 2007 年 1 月 1 日,我們在使用 Python2.5。我們剛剛發現 defaultdict 已經出現在 Python 的標準庫中了。我們可以使用 defaultdict 將字典中的值初始化為 0。我們用 defaultdict 來重寫這段程式碼
1 2 3 4 5 |
from collections import defaultdict colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = defaultdict(int) for c in colors: color_counts[c] += 1 |
for 迴圈現在相當簡單了。現在幾乎符合 Python 的程式碼風格了。
在這段程式碼中,我們發現變數 color_counts 雖然跟字典不太一樣,但是它繼承了字典所有的對映功能。
1 2 |
>>> color_counts defaultdict(<type 'int'>, {'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1}) |
我們沒有把 color_counts 這個類似於字典的物件轉換成字典的格式,因為我們覺得以後的程式碼會更加的動態。
計數
現在是2011年1月,我們在使用Python 2.7。 我們瞭解到 defaultdict已經不是最符合 python 風格的計數方式了。在 Python 2.7 版的標準庫中出現了一個 Counter 類,它可以為我們做所有的工作。
1 2 3 |
from collections import Counter colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"] color_counts = Counter(colors) |
還能變得更簡單嗎?這應該是最符合 Python 風格的程式碼了。
與 defaultdict 的方法一樣,上面的程式碼返回一個類似字典的物件(實際上是字典的子類),這與我們的目的正好不謀而合,我們就使用這個方法了。
1 2 |
>>> color_counts Counter({'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1}) |
深思之後:效能
注意一下,我們並沒有關注這些計數方法的執行效率。這些方法中大部分複雜度為 O(n),但是由於 Python 的實現方式不同,執行時間也有所差異。
儘管效能不是我們關注的主要方面,我還是很關心這些方法在 CPython 3.5.0 版下的執行時間。根據顏色在列表中出現的頻率去觀察每一種方法的執行時間是一件很有趣的事情。
結論
根據《Python之禪》(《Zen of Python》)中的格言,“應該只有一種,並且最好是唯一的方法”。 這只是一種期望。實際上並不總是隻有一種顯著的方法。方法是可以根據時間,需求和專業水平而發生改變的。
“Pythonic” 是相對的
相關資源
- import this and the Zen of Python: 從這篇文章借鑑了Python 之禪
- Permission or Forgiveness:Alex Martelli 論述了 Grace Hopper’s EAFP
- Python How To: Group and Count with Dictionaries: 寫這篇文章的時候,我發現了這篇相關的文章