Python語言中計數方法的演變

Python語言中計數方法的演變 by EarlGrey@codingpy.com

文中如對專業術語的翻譯有誤,請Python高手指正(Pythonistas),謝謝!另外,原文中的Pythonic一詞,大意指符合Python語言規範、特性和數據結構的編碼方式,蘊涵較爲豐富,爲了行文更順暢、容易理解,此文暫時用簡潔、優雅代替。-- <cite>EarlGrey@編程派</cite>html

原文連接:Trey Hunner,發佈於11月9日。
譯文連接:編程派node

有時候,利用Python語言簡潔、優雅地解決問題的方法,會隨着時間變化。隨着Python不斷進化,統計列表元素數量的方法也在改變。python

以計算元素在列表中出現的次數爲例,咱們能夠編寫出許多不一樣的實現方法。在分析這些方法時,咱們先不關注性能,只考慮代碼風格。git

要理解這些不一樣的實現方式,咱們得先知道一些歷史背景。幸運的是,咱們生活在"__future__"世界,擁有一臺時間機器。接下來,咱們一塊兒坐上時光機,回到1997年吧。github

if 語句

1997年1月1日,咱們使用的是Python 1.4。如今有一個不一樣顏色組成的列表,咱們想知道列表裏每種顏色出現的次數。咱們用字典來計算吧!express

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

注意:咱們沒有使用+=,由於增量賦值直到Python 2.0纔出現;另外,咱們也沒有使用c in color_counts這個慣用法(idiom),由於這也是Python 2.2中才發明的,編程

運行上述代碼以後,咱們會發現color_counts字典裏,如今包含了列表中每種顏色的出現次數。數據結構

color_counts
{'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1}

上面的實現很簡單。咱們遍歷了每一種顏色,並判斷該顏色是否在字典中。若是不在,就在字典加入該顏色;若是在,就增長這種顏色的計數。less

咱們還能夠把上面的代碼改寫爲:ide

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

若是列表稀疏度高(即列表中不重複的顏色數量不少),這段代碼可能運行的會有點慢。由於咱們如今要執行兩個語句,而不是一個。可是咱們不關心性能問題,咱們只關注編碼風格。通過思考,咱們決定採用新版的代碼。

try代碼塊(Code Block)

1997年1月2日,咱們使用的仍是Python 1.4。今早醒來的時候,咱們忽然意識到:咱們的代碼遵循的是「三思然後行」(Look Before You Leap,即事先檢查每一種可能出現的狀況)原則,但實際上咱們應該按照「得到諒解比得到許可容易」(Easier to Ask Forgiveness, Than Permission,即不檢查,出了問題由異常處理來處理)的原則進行編程,由於後者更加簡潔、優雅。咱們用try-except代碼塊來重構下代碼吧:

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方法。

colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]
color_counts = {}
for c in colors:
    color_counts[c] = color_counts.get(c, 0) + 1

如今,咱們的代碼會遍歷每種顏色,從字典中獲取該顏色的當前計數值。若是沒有這個計數值,則該顏色的計數值默認爲0,而後在數值的基礎上加1。最後將字典中相應鍵的值設置爲新的計數。

把主要代碼都寫在一行裏,感受很酷,可是咱們不敢徹底確定這種作法更加簡潔、優雅。咱們以爲可能有點太聰敏了,因此仍是撤銷了此次的重構。

setdefault方法

2001年1月1日,咱們如今使用的是Python 2.0。咱們據說字典類型如今有一個setdefault方法,決定利用它重構咱們的代碼。咱們還決定使用新增長的+=增量賦值運算符。

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方法。但這樣作,的確會讓代碼看上去可讀性更高。咱們發現這種方法比以前的代碼更加簡潔、優雅,因此提交了這次修改。

fromkeys方法

2004年1月1日,咱們使用的是Python 2.3。咱們據說字典新增了一個叫fromkeys的類方法(class method),能夠利用列表中的元素做爲鍵來構建字典。咱們使用新方法重構了代碼:

colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]
color_counts = dict.fromkeys(colors, 0)
for c in colors:
    color_counts[c] += 1

這段代碼將不一樣的顏色做爲鍵,建立了一個新的字典,每一個鍵的值被默認設置爲0。這樣,咱們增長每一個鍵的值時,就不用擔憂是否已經進行了設置。咱們也不須要在代碼中進行檢查或異常處理了,這看上去是個改進。咱們決定就這樣修改代碼。

推導式(Comprehension)與集合

2005年1月1日,咱們如今用的是Python 2.4。咱們發現能夠利用集合(Python 2.3中發佈,2.4版成爲內置類型)與列表推導式(Python 2.0中發佈)來解決計數問題。進一步思考以後,我想起來Python 2.4中還發布了生成器表達式(generator expressions),咱們最後決定不用列表推導式,而是採用生成器表達式。

colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]
color_counts = dict((c, colors.count(c)) for c in set(colors))

注意:咱們這裏使用的不是字典推導式,由於字典推導式直到Python 2.7才被髮明。

運行成功了,並且只有一行代碼。可是這種代碼夠簡潔、優雅嗎 ?

咱們想起了Python之禪(Zen of Python),這個Python編程指導原則起源於一個Python郵件列表,並悄悄地收進了Python 2.2.1版本中。咱們在REPL(read-eval-print loop,交互式解釋器)界面中輸入import this

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! # 命名空間是一個響亮的出色想法–就讓咱們多用用它們

譯者注:Python之禪的翻譯版本不少,這裏選用的譯文出自啄木鳥社區。

咱們的代碼變得更復雜,時間複雜度從O(n)增長到了O(n2);還變的更醜,可讀性更差了。咱們那樣改,是一次有趣的嘗試,可是一行代碼的實現形式,沒有咱們以前的方法簡潔、優雅。咱們最後仍是決定撤銷修改。

defaultdict方法

2007年1月1日,咱們使用的是Python 2.5。咱們剛發現,defaultdict已經被加入標準庫。這樣,咱們就能夠把字典的默認鍵值設置爲0了。讓咱們使用defaultdict重構代碼:

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循環如今變得真簡單!這樣確定是更加簡潔、優雅了。

咱們發現,color_counts這個變量的行爲如今有點不一樣,可是它的確繼承了字典的特性,支持全部相同的映射功能。

color_counts
defaultdict(<type 'int'>, {'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1})

咱們在這裏沒有把color_counts轉換成字典,而是假設其餘的代碼也使用鴨子類型(duck typing, Python中動態類型的一種,這裏的意思是:其餘代碼會將color_counts視做字典類型),再也不改動這個相似字典的對象。

Counter

2011年1月1日,咱們使用的是Python 2.7。別人告訴咱們,以前使用defaultdict編寫的代碼,再也不是統計顏色出現次數最簡潔、優雅的方法了。Python 2.7中新引入了一個Counter類,能夠徹底解決咱們的問題。

from collections import Counter
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]
color_counts = Counter(colors)

還有比這更簡單的方法嗎?這個必定是最簡潔、優雅的實現了。

defaultdict同樣,Counter類返回的也是一個相似字典的對象(實際是字典的一個子類)。這對知足咱們的需求來講足夠了,因此咱們就這麼幹了。

color_counts
Counter({'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1})

性能考慮

請注意,在編寫這些實現方式時,咱們都沒有關注效率問題。大部分方法的時間複雜度相同(O(n)),可是不一樣的Python語言實現形式(如CPython, PyPy,或者Jython)下,運行時間會有差別。

儘管性能不是咱們的主要關注點,我仍是在CPython 3.5.0的實現下測試了運行時間。從中,你會發現一個有趣的現象:隨着列表中顏色元素的密度(即相同元素的數量)變化,每一種實現方法的相對效率也會不一樣。

結語

根據Python之禪,「 應該有一個——寧可只有一個明顯的實現方法」。這句話所說的狀態值得追求,但事實是,並不老是隻有一種明顯的方法。這個「明顯」的方法會隨着時間、需求和專業水平,不斷地變化。

「簡潔、優雅」(即Pythonic)也是一個相對的概念。

相關文章
相關標籤/搜索