一道題讓你從此真正理解Python程式設計

寫下這個題目的時候,腦海裡無法抑制地響起了周華健那略帶沙啞的歌聲:

遠處傳來那首熟悉的歌,

那些心聲為何那樣微弱。

很久不見,你現在都還好嗎?

有沒有那麼一首歌,

會讓你輕輕跟著和,

隨著我們生命起伏,

一起唱的主題歌;

有沒有那麼一首歌,

會讓你突然想起我,

讓你歡喜也讓你憂,

這麼一個我……

音樂結束,回到正題。近日瀏覽LeetCode,發現了一道很有意思的小題目。當我嘗試用Python解答的時候,居然動用了集合、map函式、zip函式、lambda函式、sorted函式,除錯過程還涉及到了迭代器、生成器、列表推導式的概念。一個看似極為簡單的題目,儘管最終的程式碼可以合併成一行,卻幾乎把Python的程式設計技巧用了一遍,真可謂“細微之處見精神”!透過這個題目,也許會讓你從此真正理解了Python程式設計。

這道題,名為《列表中的幸運數》。什麼是幸運數呢?在整數列表中,如果一個數字的出現頻次和它的數值大小相等,我們就稱這個數字為「幸運數」。例如,在列表[1, 2, 2, 3]中,數字1和數字2出現的次數分別是1和2,所以它們是幸運數,但3只出現過1次,3不是幸運數。

明白了幸運數的概念,我們就來試著找出列表[3, 5, 2, 7, 3, 1, 2 ,4, 8, 9, 3]中的幸運數吧。這個過程可以分為以下幾個步驟:

找出列表中不重複的數字

統計每個數字在列表中出現的次數

找出出現次數等於數字本身的那些數字

第1步,找出列表中不重複的數字

找出列表中不重複的數字,也就是去除列表中的重複元素,簡稱“去重”。去重最簡潔的方法是使用集合。

第2步,統計每個數字在列表中出現的次數

我們知道,列表物件自帶一個count()方法,能返回某個元素在列表中出現的次數,具體用法如下:

接下來,我們只需要遍歷去重後的各個元素,逐一統計它們各自出現的次數,並儲存成一個合適的資料結構,這一步工作就萬事大吉了。

作為新手,程式碼寫成這樣,已經很不錯了。但是,一個有追求的程式設計師絕對不會就此自滿、裹足不前。他們最喜歡做的事情就是想盡千方百計消滅for迴圈,比如使用對映函式、過濾函式取代for迴圈;即便不能拒絕for迴圈,他們也會盡可能把迴圈藏起來,比如藏在列表推導式內。這裡既然是要對每一個元素都呼叫列表的count()這個方法,那就最適合用map函式取代for迴圈了。

map函式返回的是一個生成器(generator),可以像列表一樣遍歷,但無法像列表那樣直觀地看到各個元素,除非我們用list()把這個生成器轉成列表(實際上並不需要將生成器轉為列表)。請注意,生成器和迭代器不同,或者說生成器是一種特殊的迭代器,只能被遍歷一次,遍歷結束,就自動消失了。迭代器則可以反覆遍歷。比如,range()函式返回的就是迭代器:

說完生成器和迭代器,咱們還得回到原來的話題上。使用map對映函式,我們得到了每個元素的出現次數,還需要和對應的元素組成一個一個的元組。這時候,就用上zip()函數了。zip() 函式建立一個生成器,用來聚合每個可迭代物件(迭代器、生成器、列表、元組、集合、字串等)的元素,元素按照相同下標聚合,長度不同則忽略大於最短迭代物件長度的元素。

很顯然,zip()函式返回的也是生成器,只能用一次,過後即消失。

第3步,找出出現次數等於數字本身的那些數字

有了每個元素及其出現的次數,我們只需要迴圈遍歷……不,請稍等,我們為什麼一定要迴圈呢?我們只是要把每個元素過濾一遍,找出那些出現次數等於元素自身的那些元組,為什麼不試試過濾函式filter()呢?

過濾函式filter()接受兩個引數,第1個引數是個函式,用於判斷一個元素是否符合過濾條件,第2個引數就是需要過濾的可迭代物件了。filter()函式返回的也是生成器,只能用一次,過後即消失。

寫這裡,我們幾乎要大功告成了。但是,作為一個有追求的程式設計師,你能容忍func()這樣一個看起來怪怪的函式嗎?答案是不能!你一定會用lambda函式取代它。另外,也許我們還需要對結果按照元素的大小排序。加上排序,完整程式碼如下:

終極程式碼,一行搞定

如果你曾經有過被那些寫成一行、卻能實現複雜功能的、看起來像天書一樣的程式碼蹂躪的痛苦經歷,那麼,現在你也可以把上面的程式碼寫成一行,去蹂躪別人了。

戲劇性反轉,這次真的理解Python了!

有人說,何必那麼麻煩呢?這樣寫不是更簡單、更易讀嗎?果然,我真是想多了!