當前位置:
首頁 > 知識 > 寫了三年代碼,還是不懂 Python 世界的規則

寫了三年代碼,還是不懂 Python 世界的規則

前言

編程,其實和玩電子遊戲有一些相似之處。你在玩不同遊戲前,需要先學習每個遊戲的不同規則,只有熟悉和靈活運用遊戲規則,才更有可能在遊戲中獲勝。

而編程也是一樣,不同編程語言同樣有著不一樣的「規則」。大到是否支持面向對象,小到是否可以定義常量,編程語言的規則比絕大多數電子遊戲要複雜的多。

當我們編程時,如果直接拿一種語言的經驗套用到另外一種語言上,很多時候並不能取得最佳結果。這就好像一個 CS(反恐精英) 高手在不了解規則的情況下去玩 PUBG(絕地求生),雖然他的槍法可能萬中無一,但是極有可能在發現第一個敵人前,他就會倒在某個窩在草叢裡的敵人的伏擊下。

Python 里的規則

Python 是一門初見簡單、深入後愈覺複雜的語言。拿 Python 里最重要的「對象」概念來說,Python 為其定義了多到讓你記不全的規則,比如:

定義了 方法的對象,就可以使用 函數來返回可讀名稱

定義了 和 方法的對象,就可以被循環迭代

定義了 方法的對象,在進行布爾判斷時就會使用自定義的邏輯

... ...

熟悉規則,並讓自己的代碼適應這些規則,可以幫助我們寫出更地道的代碼,事半功倍的完成工作。下面,讓我們來看一個有關適應規則的故事。

案例:從兩份旅遊數據中獲取人員名單

某日,在一個主打紐西蘭出境游的旅遊公司里,商務同事突然興沖沖的跑過來找到我,說他從某合作夥伴那裡,要到了兩份重要的數據:

所有去過「泰國普吉島」的人員及聯繫方式

所有去過「紐西蘭」的人員及聯繫方式

數據採用了 JSON 格式,如下所示:

每份數據裡面都有著 、 、 、 四個欄位。基於這份數據,商務同學提出了一個(聽上去毫無道理)的假設:「去過普吉島的人,應該對去紐西蘭旅遊也很有興趣。我們需要從這份數據里,找出那些去過普吉島但沒有去過紐西蘭的人,針對性的賣產品給他們。

第一次蠻力嘗試

有了原始數據和明確的需求,接下來的問題就是如何寫代碼了。依靠蠻力,我很快就寫出了第一個方案:

因為原始數據里沒有「用戶 ID」之類的唯一標示,所以我們只能把「姓名和電話號碼完全相同」作為判斷是不是同一個人的標準。

函數通過循環的方式,先遍歷所有去過普吉島的人,然後再遍歷紐西蘭的人,如果在紐西蘭的記錄中找不到完全匹配的記錄,就把它當做「潛在客戶」返回。

這個函數雖然可以完成任務,但是相信不用我說你也能發現。它有著非常嚴重的性能問題。對於每一條去過普吉島的記錄,我們都需要遍歷所有紐西蘭訪問記錄,嘗試找到匹配。整個演算法的時間複雜度是可怕的 ,如果紐西蘭的訪問條目數很多的話,那麼執行它將耗費非常長的時間。

為了優化內層循環性能,我們需要減少線性查找匹配部分的開銷。

嘗試使用集合優化函數

如果你對 Python 有所了解的話,那麼你肯定知道,Python 里的字典和集合對象都是基於 哈希表(Hash Table) 實現的。判斷一個東西是不是在集合里的平均時間複雜度是 ,非常快。

所以,對於上面的函數,我們可以先嘗試針對紐西蘭訪問記錄初始化一個集合,之後的查找匹配部分就可以變得很快,函數整體時間複雜度就能變為 。

讓我們看看新的函數:

使用了集合對象後,新函數在速度上相比舊版本有了飛躍性的突破。但是,對這個問題的優化並不是到此為止,不然文章標題就應該改成:「如何使用集合提高程序性能」 了。

對問題的重新思考

讓我們來嘗試重新抽象思考一下問題的本質。首先,我們有一份裝了很多東西的容器 A(普吉島訪問記錄),然後給我們另一個裝了很多東西的容器 B(紐西蘭訪問記錄),之後定義相等規則:「姓名與電話一致」。最後基於這個相等規則,求 A 和 B 之間的「差集」

如果你對 Python 里的集合不是特別熟悉,我就稍微多介紹一點。假如我們擁有兩個集合 A 和 B,那麼我們可以直接使用 這樣的數學運算表達式來計算二者之間的差集

所以,計算「所有去過普吉島但沒去過紐西蘭的人」,其實就是一次集合的求差值操作。那麼要怎麼做,才能把我們的問題套入到集合的遊戲規則里去呢?

利用集合的遊戲規則

在 Python 中,如果要把某個東西裝到集合或字典里,一定要滿足一個基本條件:「這個東西必須是可以被哈希(Hashable)的」。什麼是 「Hashable」?

舉個例子,Python 裡面的所有可變對象,比如字典,就不是Hashable 的。當你嘗試把字典放入集合中時,會發生這樣的錯誤:

所以,如果要利用集合解決我們的問題,就首先得定義我們自己的 「Hashable」 對象:。而要讓一個自定義對象變得 Hashable,唯一要做的事情就是定義對象的 方法。

一個好的哈希演算法,應該讓不同對象之間的值儘可能的唯一,這樣可以最大程度減少「哈希碰撞」發生的概率,默認情況下,所有 Python 對象的哈希值來自它的內存地址。

在這個問題里,我們需要自定義對象的 方法,讓它利用 元組作為 類的哈希值來源。

自定義完 方法後, 實例就可以正常的被放入集合中了。但這還不夠,為了讓前面提到的求差值演算法正常工作,我們還需要實現 特殊方法。

是 Python 在判斷兩個對象是否相等時調用的特殊方法。默認情況下,它只有在自己和另一個對象的內存地址完全一致時,才會返回 。但是在這裡,我們復用了 對象的哈希值,當二者相等時,就認為它們一樣。

完成了恰當的數據建模後,之後的求差值運算便算是水到渠成了。新版本的函數只需要一行代碼就能完成操作:

Hint:如果你使用的是 Python 2,那麼除了 方法外,你還需要自定義類的 (判斷不相等時使用) 方法。

使用 dataclass 簡化代碼

故事到這裡並沒有結束。在上面的代碼里,我們手動定義了自己的數據類,實現了 、 等初始化方法。但其實還有更簡單的做法。

因為定義數據類這種需求在 Python 中實在太常見了,所以在 3.7 版本中,標準庫中新增了 dataclasses 模塊,專門幫你簡化這類工作。

如果使用 dataclasses 提供的特性,我們的代碼可以最終簡化成下面這樣:

不用干任何臟活累活,只要不到十行代碼就完成了工作。

案例總結

問題解決以後,讓我們再做一點小小的總結。在處理這個問題時,我們一共使用了三種方案:

使用普通的兩層循環篩選符合規則的結果集

利用哈希表結構(set 對象)創建索引,提升處理效率

將數據轉換為自定義對象,利用規則,直接使用集合運算

為什麼第三種方式會比前面兩種好呢?

首先,第一個方案的性能問題過於明顯,所以很快就會被放棄。那麼第二個方案呢?仔細想想看,方案二其實並沒有什麼明顯的缺點。甚至和第三個方案相比,因為少了自定義對象的過程,它在性能與內存佔用上,甚至有可能會微微強於後者。

但請再思考一下,如果你把方案二的代碼換成另外一種語言,比如 Java,它是不是基本可以做到 1:1 的完全翻譯?換句話說,它雖然效率高、代碼直接,但是它沒有完全利用好 Python 世界提供的規則,最大化的從中受益。

如果要具體化這個問題里的「規則」,那就是「Python 擁有內置結構集合,集合之間可以進行差值等四則運算」這個事實本身。匹配規則後編寫的方案三代碼擁有下面這些優勢:

為數據建模後,可以更方便的定義其他方法

如果需求變更,做反向差值運算、求交集運算都很簡單

理解集合與 dataclasses 邏輯後,代碼遠比其他版本更簡潔清晰

如果要修改相等規則,比如「只擁有相同姓的記錄就算作一樣」,只需要繼承 覆蓋 方法即可

其他規則如何影響我們

在前面,我們花了很大的篇幅講了如何利用「集合的規則」來編寫事半功倍的代碼。除此之外,Python 世界中還有著很多其他規則。如果能熟練掌握這些規則,就可以設計出符合 Python 慣例的 API,讓代碼更簡潔精鍊。

下面是兩個具體的例子。

使用 做對象字元串格式化

如果你的自定義對象需要定義多種字元串表示方式,就像下面這樣:

那麼除了增加這種 額外方法外,你還可以嘗試自定義 類的 方法,因為那才是將對象變為字元串的標準規則。

使用 定義對象切片操作

如果你要設計某個可以裝東西的容器類型,那麼你很可能會為它定義「是否為空」、「獲取第 N 個對象」等方法:

但是,這樣並非最好的做法。因為 Python 已經為我們提供了一套對象規則,所以我們不需要像寫其他語言的 OO(面向對象) 代碼那樣去自己定義額外方法。我們有更好的選擇:

新的寫法相比舊代碼,更能適配進 Python 世界的規則,API 也更為簡潔。

關於如何適配規則、寫出更好的 Python 代碼。Raymond Hettinger 在 PyCon 2015 上有過一次非常精彩的演講 「Beyond PEP8 - Best practices for beautiful intelligible code」。這次演講長期排在我個人的 「PyCon 視頻 TOP5」 名單上,如果你還沒有看過,我強烈建議你現在就去看一遍 :)

Hint:更全面的 Python 對象模型規則可以在 官方文檔 找到,有點難讀,但值得一讀。

總結

Python 世界有著一套非常複雜的規則,這些規則的涵蓋範圍包括「對象與對象是否相等「、」對象與對象誰大誰小」等等。它們大部分都需要通過重新定義「雙下劃線方法 」 去實現。

如果熟悉這些規則,並在日常編碼中活用它們,有助於我們更高效的解決問題、設計出更符合 Python 哲學的 API。下面是本文的一些要點總結:

永遠記得對原始需求做抽象分析,比如問題是否能用集合求差集解決

如果要把對象放入集合,需要自定義對象的 與 方法

方法決定性能(碰撞出現概率), 決定對象間相等邏輯

使用 dataclasses 模塊可以讓你少寫很多代碼

使用 方法替代自己定義的字元串格式化方法

在容器類對象上使用 、 方法,而不是自己實現

看完文章的你,有沒有什麼想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。

回復下方「關鍵詞」,獲取優質資源

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 編程派 的精彩文章:

Python的51 個秘密曝光,Github獲2 萬星
寫 Python 時你要避免的十個錯誤

TAG:編程派 |