如何編寫 Python 文件生成器

發表於2016-12-09

在我剛開始接觸Python的日子裡,我最喜歡做的事情之一是坐在直譯器旁使用內建help功能來檢查類和方法,決定下一個要敲的內容。這個功能匯入一個物件,遍佈它的成員,取出文件註釋,生成一個類似manpage的輸出,從而幫助你找到如何使用正在檢查的物件的方法。

它被內建成一個標準庫的美妙之處在於通過程式碼直接生成輸出,它為我這樣的懶人間接地強調了一個編碼風格,我就想著在儘量少做額外的工作的情況下維護文件。尤其是如果你已經為你的變數和函式選擇直接的名字。 這種風格涉及到向你的函式和類新增文件字串,以及通過用下劃線字首來正確地識別私有成員和受保護成員。

在Python直譯器上執行help(list)的輸出。

然而,事實上還沒有一種從原始碼生成 markdown 的預設方式,除了一些外掛。後來,我不斷在谷歌上查詢,我還是不滿意我發現的外掛——很多東西都過時了,沒有人維護了,或者輸出的東西不是我需要的——因此,我決定寫一個我自己的專案。我認為這很有意思,這也讓我學到了更多關於構建和除錯一個模組的知識,更多內容可以檢視我之前的文章( 設計一個簡單的圖形 Python 偵錯程式):inspect 模組。

“inspect 模組提供了幾個有用的函式來幫助我們獲取生存著的物件資訊…  ” — Python 文件

檢查!

Inspect,源自於標準程式庫,它不僅允許你檢視較低階別的 python 框架和程式碼物件,它還提供很多方法來檢查模組和類,幫你發現可能感興趣的的專案。這個也就是之前提到的用來生成幫助檔案的 pydoc。

瀏覽一下線上文件,你會發現許多跟我們所做的嘗試相關的方法。最重要的幾個是getmembers(),getdoc() 和 signature(),還有許多給 getmembers 做濾波器的 is… 功能。擁有這些,我們可以輕易地迴圈訪問很多功能,包括區分生成器和協同程式,並可以按需要遞迴到任何一個類以及內部。

匯入編碼

如果我們要去檢查一個物件,不管它是什麼,第一步要做的是提供一個匯入進我們的名稱空間的原理。為何要討論匯入呢?這取決於你想要做什麼,還有很多需要擔心的,包括虛擬環境,自定義程式碼,標準模組和重新命名。情況會容易混淆,搞錯的話會需要一些時間去整理清楚。

我們當然還有些選擇,更復雜的是直接從pydoc重用safeimport(),當出現問題時,為我們解決很多特例和ErrorDuringImport類的特別條款。然而,如果我們對我們的環境需要更高的控制,我們自己簡單地執行__import__(modulename)也是可能的。

另一個需要記住的是每一個程式碼的執行路徑。可能會用到 sys.path.append() 的一個目錄來進入我們尋找的模組。我的用例我的用例是從命令列和被檢查的模組的路徑中的目錄執行,所以,我將當前目錄新增到 sys.path,這足以解決典型的匯入路徑問題。

按照上述方式,我們的匯入函式會如下所示:

決定輸出

在繼續之前,你需要一個關於如何組織生成 markdown 輸出的心理影像。思考:你需要一個不遞迴到自定義類的淺的引用嗎?我們想要包含哪些方法?內建功能會怎麼樣?是用_還是__方法?我們應該如何呈現函式簽名?我們應該拉註釋嗎?

我的選擇如下:

  • 每個執行一個 .md 檔案,其中包含遞迴到正在檢查的物件的任意子類中生成的資訊。
  • 只包括我建立的自定義程式碼,沒有來自匯入的模組的資訊。
  • 每一項的輸出必須用第二級 markdown 標題(##)標識。
  • 所有標頭檔案必須包含正在描述的項的完整路徑(module.class.subclass.function)。
  • 將完整的函式簽名作為預格式化文字。
  • 為每個標題提供錨點,以便輕鬆的連結到文件及文件本身內容。
  • 任何以_或者__開頭的函式都不做文件記錄。

整合在一起

一旦物件被匯入,我們可以開始檢測了。這是一個簡單的例子,重複呼叫 getmembers(object, filter),過濾器是一個有用的 is 函式。你能夠發現 isclass 和 isfunction,其它相關的方法都是 is開頭的,例如,ismethod, isgenerator, iscoroutine。這都取決於你是否想寫一些通用的,可以處理所有的特殊情況,或一些更小的和更特殊的原始碼。我堅持前兩點,因為我不用把採用3個不同方法來建立我想要的格式化模組,類和功能。

當要格式化大文字和一些程式設計程式碼的混合體時,我傾向於把它作為一個在列表或元組中的獨立專案,用 “”.join() 來合併輸出。在寫作的時候,這種方法其實比基於插值的方法(.format 和 %)更快。然而,python 3.6 的新字串格式化將比這更快,更具可讀性。

你可以看到,getmembers() 返回一個元組與物件的名稱在第一位置和第二位置的實際物件,我們可以用遞迴遍歷物件層次。

對於檢索到的每一個專案,可能使用 getdoc() 或 getcomments() 獲取文件字串和註釋。對於每一個功能,我們可以使用 signature() 得到 Signature 物件 ,它表示其位置引數和關鍵字引數的預設值和任何註釋,為我們提供了產生簡單直接的描述和良好風格的文字,有助於我們理解使用者我們寫程式碼的意圖。

其他考慮因素和非預期後果

請注意,上面的程式碼只是示例程式碼,只是讓你大概真的最終產品應該是什麼樣子。在最終確定產品之前,還有很多其他注意事項:

  • getfunctions 和 getclasses 將顯示模組中匯入的所有方法和類。包括內建程式包,以及來自外部軟體包的任何東西,所以你必須過濾掉更多的 for 迴圈。我在檢查過程中使用模組的 __file__ 屬性,不管它包含什麼項。換句話說,如果項在我正在執行的路徑中存在的模組內定義,則包含它(使用 os.path.commonprefix())。
  • 有一些 gotcha 的檔案路徑,匯入層次結構和名稱。像通過 __init__.py 將 moduleX 匯入到包中時,你可以通過 package.moduleX.function 訪問他的函式方法,但是全稱將會是 package.moduleX.moduleX.function—通過 moduleX.__name__ 返回的名稱。你或許不在乎這個區別,但是我在乎,所以這是在迭代過程中需要記住的事情。
  • 你會從內建程式庫中匯入類和任何其他不包含 __file__ 的東西,如果你進行任何如上所述的過濾,那麼檢查是必要的。
  • 因為這是 markdown,而我們只是匯入 docstrings,你可以在你的 docstrings 中包含 mardown 語法,它會美觀漂亮的呈現在頁面中。然而,這意味著你應該注意正確的轉義 docstrings,這樣他才不會破壞生成的 HTML。

    示例輸出

    我在 sofi 包-精確的說是 sofi.app 模組執行生成器,下面是它建立的 markdown 內容。

    下面是通過 mkdocs 執行它產生 readthedocs 主題頁面後的最終結果(不包括函式註釋)的示例。153256_g2av_2903254

    我相信你已經知道,使用這些機制自動生成文件,會生成完整、精確和最新的模組資訊,這些資訊在你編寫程式碼的時候可以進行維護和編寫,且操作簡單。我強烈建議每個人都試一試。在結束之前,我想再補充一點,mkdocs 並不是唯一的文件包,還有其他一些使用廣泛的系統,如 Sphinx(mkdocs 基於此開發)和 Doxygen,他們都能實現我們以上討論的事項。然而,我比較通過練習來學習和了解更多關於 Python 內部機制和其隨附的工具。

相關文章