“容器”這兩個字很少被 Python 技術文章提起。一看到“容器”,大家想到的多是那頭藍色小鯨魚:Docker,但這篇文章和它沒有任何關係。本文里的容器,是 Python 中的一個抽象概念,是對專門用來裝其他對象的數據類型的統稱。 在 Python 中,有四類最常見的內建容器類型: 列表(list) ...
“容器”這兩個字很少被 Python 技術文章提起。一看到“容器”,大家想到的多是那頭藍色小鯨魚:Docker,但這篇文章和它沒有任何關係。本文里的容器,是 Python 中的一個抽象概念,是對專門用來裝其他對象的數據類型的統稱。
在 Python 中,有四類最常見的內建容器類型: 列表(list)、 元組(tuple)、 字典(dict)、 集合(set)。通過單獨或是組合使用它們,可以高效的完成很多事情。
Python 語言自身的內部實現細節也與這些容器類型息息相關。比如 Python 的類實例屬性、全局變數 globals() 等就都是通過字典類型來存儲的。
在這篇文章里,我首先會從容器類型的定義出發,嘗試總結出一些日常編碼的最佳實踐。之後再圍繞各個容器類型提供的特殊機能,分享一些編程的小技巧。
當我們談論容器時,我們在談些什麼?
我在前面給了“容器”一個簡單的定義:專門用來裝其他對象的就是容器。但這個定義太寬泛了,無法對我們的日常編程產生什麼指導價值。要真正掌握 Python 里的容器,需要分別從兩個層面入手:
- 底層實現:內置容器類型使用了什麼數據結構?某項操作如何工作?
- 高層抽象:什麼決定了某個對象是不是容器?哪些行為定義了容器?
下麵,讓我們一起站在這兩個不同的層面上,重新認識容器。
底層看容器
Python 是一門高級編程語言,它所提供的內置容器類型,都是經過高度封裝和抽象後的結果。和“鏈表”、“紅黑樹”、“哈希表”這些名字相比,所有 Python 內建類型的名字,都只描述了這個類型的功能特點,其他人完全沒法只通過這些名字瞭解它們的哪怕一丁點內部細節。
這是 Python 編程語言的優勢之一。相比 C 語言這類更接近電腦底層的編程語言,Python 重新設計並實現了對編程者更友好的內置容器類型,屏蔽掉了記憶體管理等額外工作。為我們提供了更好的開發體驗。
但如果這是 Python 語言的優勢的話,為什麼我們還要費勁去瞭解容器類型的實現細節呢?答案是:關註細節可以幫助我們編寫出更快的代碼。
寫更快的代碼
1. 避免頻繁擴充列表/創建新列表
所有的內建容器類型都不限制容量。如果你願意,你可以把遞增的數字不斷塞進一個空列表,最終撐爆整台機器的記憶體。
在 Python 語言的實現細節里,列表的記憶體是按需分配的[註1],當某個列表當前擁有的記憶體不夠時,便會觸發記憶體擴容邏輯。而分配記憶體是一項昂貴的操作。雖然大部分情況下,它不會對你的程式性能產生什麼嚴重的影響。但是當你處理的數據量特別大時,很容易因為記憶體分配拖累整個程式的性能。
還好,Python 早就意識到了這個問題,並提供了官方的問題解決指引,那就是:“變懶”。
如何解釋“變懶”? range() 函數的進化是一個非常好的例子。
在 Python 2 中,如果你調用 range(100000000),需要等待好幾秒才能拿到結果,因為它需要返回一個巨大的列表,花費了非常多的時間在記憶體分配與計算上。但在 Python 3 中,同樣的調用馬上就能拿到結果。因為函數返回的不再是列表,而是一個類型為 range 的懶惰對象,只有在你迭代它、或是對它進行切片時,它才會返回真正的數字給你。
所以說,為了提高性能,內建函數 range “變懶”了。而為了避免過於頻繁的記憶體分配,在日常編碼中,我們的函數同樣也需要變懶,這包括:
- 更多的使用 yield 關鍵字,返回生成器對象
- 儘量使用生成器表達式替代列表推導表達式
- 生成器表達式: (iforinrange(100))
- 生成器表達式: (iforinrange(100))