Python高效代碼實踐:性能、內存和可用性(譯)
文章翻譯自 Python Practices for Efficient Code: Performance, Memory, and Usability。
遵循最佳做法的代碼庫在當今世界能得到高度評價。如果您的項目是開源的,這會是一個吸引優秀開發人員的方式。作為開發人員,您想要編寫高效且優化的代碼:
佔用儘可能小的內存、執行地更快、看起來整潔、文檔正確、遵循標準風格指南,並且易於被新開發者理解。
這裡討論的實踐可能有助於您為開源組織做出貢獻,向在線評審(Online Judge)提交解決方案,使用機器學習處理大量數據處理問題,或開發自己的項目。
實踐 1:盡量不要對內存置之不理!
一個簡單的 Python 程序在內存上可能不會引起很多問題,但在高內存消耗的項目中內存使用變得至關重要。從一開始開發大項目時,合理使用內存是明智的。
與 C/C ++ 不同,Python 解釋器會進行內存管理,用戶無法自己控制。Python 中的內存管理涉及包含所有Python對象和數據結構的專用堆。
Python 內存管理器內部確保對這個專用堆的管理。當您創建對象時,Python 虛擬機處理所需的內存,並決定將其放置在內存布局中的特定位置。
然而,如何更好地了解事情的工作原理和不同的方法來做事情,可以幫助您最大限度地減少程序的內存使用量。
- 使用生成器來計算大量的結果
生成器可進行惰性計算。您可以通過遍歷來使用它們:顯示地使用 「for」 或者隱式地將其傳遞給任何方法或構造。
生成器可以返回多個項,就像返回一個列表 —— 不是一次返回所有,而是一個接一個地返回。 生成器會暫停,直到下一個項被請求。在 這裡 閱讀更多關於 Python 生成器的內容。
- 對於大量數字/數據的處理,您可以使用像 Numpy 這樣的庫,它可以優雅地處理內存管理。
- 使用 format 而不是 「+」 來生成字元串 —— 在Python中,str 是不可變的,所以每對連接都必須將左、右字元串複製到新的字元串中。如果連接長度為10的四個字元串,則將複製(10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 個字元,而不是 40 字元。隨著字元串數量和大小的增加,事情會變得越來越糟。Java 有時將一系列的連接轉換為使用StringBuilder 來優化這種情況,但是 CPython 沒有。因此,建議使用 .format 或 % 語法。如果您不能在 .format 和 % 之間選擇,請查看 這個 有趣的 StackOverflow 問題。
- 定義 Python 類時使用槽(slots)。您可以通過將類中的 __slots__ 設置為固定的屬性名稱列表,來告訴 Python 不要使用動態字典,只為一組固定的屬性分配空間,從而消除了為每個對象使用一個字典的開銷。在 這裡 閱讀更多關於槽的內容。
- 您可以通過使用內置的模塊(如 resource 和 objgraph)來跟蹤對象級別的內存使用情況。
- 在 Python 中管理內存泄漏可能是一項艱巨的任務,但幸運的是有一些工具(如 heapy)用於調試內存泄漏。Heapy 可以與 objgraph 一起使用來觀察 diff 對象的分配隨時間而增長。Heapy 可以顯示哪些對象佔用最多的內存。Objgraph 可以幫助您找到反向引用,以明白為什麼它們不能被釋放。您可以在 這裡 閱讀更多關於在Python中診斷內存泄漏的信息。
您可以在 這裡 閱讀由 Theano 的開發人員編寫的關於 Python 內存管理的細節。
實踐2:Python2 還是 Python3?
當開始一個新的 Python 項目,或是只學習 Python,您可能會發現自己在選擇 Python2 還是Python3 上十分糾結。這是一個廣泛討論的話題,在網上有許多觀點和好的解釋。
一方面,Python3 有一些很棒的新特性。另一方面,您可能希望使用僅支持 Python2 的包,而Python3 不能向後兼容。這意味著在 Python3.x 的解釋器上運行 Python2 的代碼可能會拋出錯誤。
不過,編寫能同時跑在 Python2 和 Python3 解釋器的代碼是可能的。最常見的方法是使用_future、builtins 和 six 這樣的軟體包來維護一個簡單、乾淨的 Python3.x 兼容代碼庫,能以最小的開銷同時支持Python2 和 Python3。
python-future 是 Python2 和 Python3 之間的缺失兼容層。它提供 future 和 past 的包,能夠向前或向後移植 Python2 和 Python3 的特性。它還帶有 futurize 和 pasteurize,定製化的 2 到 3 基礎的腳本,可以幫助您輕鬆地將 Py2 或 Py3 代碼逐模塊轉換為乾淨的支持 Python2 和 Python3 的Py3 風格的代碼庫。
請查看 Ed Schofield 編寫的超贊的 Python 2-3 兼容代碼 手抄冊(需翻牆)。 如果相比閱讀,您更喜歡視頻,可以在 PyCon AU 2014 上找到他的演講,「編寫 2/3 兼容的代碼」(需翻牆)。
實踐3:寫出美麗的代碼,因為「第一印象即是最後印象」
分享代碼是一個有益的嘗試。無論什麼動機,如果人們發現您的代碼難以使用或理解,那麼您的良好意圖可能沒有達到預期。幾乎每個組織都遵循開發人員必須遵循的風格指南,以保持一致性、易於調試和協作。Python 的禪就像一個迷你風格的 Python 設計指南。主流的 Python 風格指南包括:
- PEP-8 風格指南
- Python 習語和效率
- Google Python 風格指南
這些準則討論了如何使用:空格、逗號和大括弧,對象命名指南等。儘管它們在某些情況下可能發生衝突,但它們都具有相同的目標 —— 「清晰、可讀和可調試的代碼標準」。
堅持一個指南,或遵循自己的,但不要試圖跟隨與廣泛接受的標準大不相同的內容。
使用靜態代碼分析工具
有許多可用的開源工具能夠使您的代碼符合標準的風格指南和編寫代碼的最佳實踐。
Pylint 是一個 Python 工具,用於檢查模塊的編碼標準。Pylint 可以快速輕鬆地查看您的代碼是否捕捉到了 PEP-8 的精髓,因此對其他潛在用戶是「友好的」。
它還為您提供優良的指標和統計報告,可幫助您判斷代碼質量。您還可以通過創建自己的 .pylintrc 文件進行自定義和使用。
Pylint 不是唯一的選擇 —— 還有其他工具,如 PyChecker,PyFlakes 以及像 pep8 和 flakes8 這樣的包。
我的建議是使用 coala,一個統一的靜態代碼分析框架,旨在通過單個框架提供語言非特定的代碼分析。Coala 支持我之前提到的所有的linting工具,並且是高度可定製的。
正確地文檔說明代碼
這方面對您的代碼庫的可用性和可讀性至關重要。始終建議您儘可能廣泛地文檔說明您的代碼,以便其他開發人員更容易了解您的代碼。
功能的典型內聯文檔應包括:
- 該功能的一行概要。
- 如果適用的話,提供交互示例。這些可以讓新開發人員參考,以快速了解功能的使用和預期的輸出。您也可以使用 doctest 模塊來確保這些示例的正確性(以測試方式運行)。請參閱 doctest 文檔 中的示例。
- 參數文檔(通常一行描述參數及其在函數中的作用)
- 返回類型的文檔(除非您的函數沒有返回任何內容!)
Sphinx 是廣泛使用的用於生成和管理項目文檔的工具。它提供了大量方便的功能,可以減少您編寫標準文檔的工作量。此外,您可以將文檔推送到 Read the Docs,這是最常用的託管項目文檔的方式。
Hitchikers guide to Python for documentation (筆者翻譯成了中文版——Python 最佳實踐指南)包含一些有趣的信息,在文檔說明代碼時可能對您有用。
實踐4:提高性能
多進程,而不是多線程
改進多任務代碼的執行時間時,您可能希望利用 CPU 中的多核同時執行多個任務。產生幾個線程並讓它們並發執行可能看起來很直觀,但是由於 Python 中的全局解釋器鎖,所有的線程都是在相同的核上輪流運行。
為了實現 Python 的實際並行化,您可能需要使用 Python 的 multiprocessing 模塊。另一個解決方案可以是將任務外包給:
- 操作系統(通過多進程)
- 一些調用您的 Python 代碼的外部應用程序(例如 Spark 或 Hadoop)
- 您的Python代碼所調用的代碼(例如,您可以讓 Python 代碼調用C函數,來執行昂貴的多線程內容)。
除了並行,還有其他方法可以提高您的性能。其中一些包括:
- 使用最新版本的 Python:這是最直接的方法,因為新的更新通常包括對已經存在功能性能方面的增強。
- 儘可能使用內置函數:這也符合 DRY 原則 —— 內置函數由世界上一些最好的 Python 開發人員仔細設計和審查,所以它們通常是最好的方式。
- 考慮使用 Ctypes:Ctypes 提供了一個在 Python 代碼中調用 C 共享函數的介面。C 是一種更接近機器級別的語言,與 Python 中的類似實現相比,代碼執行速度更快。
- 使用 Cython:Cython 是一種 Python 語言的超集,允許用戶調用 C 函數並具有靜態類型聲明,最後生成一份更簡單的最終代碼,可能會執行得快得多。
- 使用 PyPy:PyPy 是具有 JIT(即時)編譯器的另一個 Python 實現,可以使您的代碼執行更快。雖然我從未嘗試過 PyPy,但它也聲稱會減少程序的內存消耗。像 Quora 這樣的公司實際上在生產環境中使用 PyPy。
- 設計與數據結構:適用於各種語言。確保您正在為目標使用正確的數據結構,在正確的地方聲明變數,明智地利用標識符範圍,並在任何有意義的地方緩存結果等。
我可以給出的一個具體的例子是:Python 通常在訪問全局變數和解析函數地址時很慢,所以將它們分配到當前作用域內的一個局部變數,然後訪問它們,速度會更快。
實踐5:分析您的代碼
通常,分析您的代碼的覆蓋度、質量和性能是有幫助的。Python 附帶了 cProfile 模塊來幫助評估性能。它不僅給出了總運行時間,還分別對每個函數進行了計時。
然後,它會告訴您每個函數調用的時間,這樣可以很容易地確定要優化的地方。以下是cProfile 的一個示例分析:
- memory_profiler 是一個用於監視進程內存消耗的Python模塊,它能對 Python 程序的內存消耗進行逐行分析。
- objgraph 能顯示前N個佔用 Python 程序內存的對象、在一段時間內刪除或添加的對象以及腳本中給定對象的所有引用。
- resource 為程序測量和控制系統資源使用提供了基本機制。該模塊的兩個主要用途包括限制資源分配和獲取有關資源當前使用情況的信息。
實踐6:測試和持續集成
測試:
寫單元測試是個好習慣。如果您認為寫測試不值得您努力,請查看此 StackOverflow 問題。最好在編碼之前或期間編寫測試。Python 提供了unittest 模塊來為函數和類編寫單元測試。此外還有如下框架:
- nose —— 可以運行 unittest 測試,並具有較少的樣板。
- pytest —— 也運行unittest測試,更少的樣板,更好的報告和很多很酷,額外的功能。
為了得到良好的比較,請閱讀這裡的介紹。
不要忘記 doctest 模塊,它使用內聯文檔中的互動式示例來測試源代碼。
測量覆蓋度:
Coverage 是測量 Python 程序代碼覆蓋度的工具。它監控您的程序,關注代碼的哪些部分已被執行,然後分析源碼以識別可能已被執行但沒有執行的代碼。
覆蓋度測量通常用於測量測試的有效性。它可以顯示您的代碼的哪些部分被測試執行了,哪些沒有。通常建議有 100% 的分支覆蓋度,這意味著您的測試應該能夠執行和驗證項目的每個分支的輸出。
持續集成:
從一開始就為您的項目配置 CI 系統,對於您的項目來說可以非常有用。您可以使用 CI 服務輕鬆測試代碼庫的各個方面。CI 中的一些典型檢查包括:
- 在現實環境中運行測試。有些情況下,測試在某些架構上通過,而在其他架構上失敗。CI 服務可以讓您在不同的系統架構上運行測試。
- 對您的代碼庫執行覆蓋度約束。
- 構建和部署您的代碼到生產環境(您可以在不同的平台上這樣做)
現今有一些 CI 服務:一些最受歡迎的有Travis、Circle(適用於OSX和Linux)和Appveyor(適用於Windows)。根據我最初的使用,像 Semaphore CI 這樣的新興產品看起來是可靠的。Gitlab(另一個Git存儲庫管理平台,如 Github)也支持 CI,不過如同其他服務一樣,您需要明確配置它。
推薦閱讀:
※對一些盲目想從事大數據的朋友的警示。
※有沒有什麼軟體能夠在Win10系統下將電腦重複的工作自動實現?
※20170402Python基礎知識的梳理總結
※spyder 如何添加和安裝其他的包?