在我剛開始接觸Python的日子裡,我最喜歡做的事情之一是坐在直譯器旁使用內建help功能來檢查類和方法,決定下一個要敲的內容。這個功能匯入一個物件,遍佈它的成員,取出文件註釋,生成一個類似manpage的輸出,從而幫助你找到如何使用正在檢查的物件的方法。
它被內建成一個標準庫的美妙之處在於通過程式碼直接生成輸出,它為我這樣的懶人間接地強調了一個編碼風格,我就想著在儘量少做額外的工作的情況下維護文件。尤其是如果你已經為你的變數和函式選擇直接的名字。 這種風格涉及到向你的函式和類新增文件字串,以及通過用下劃線字首來正確地識別私有成員和受保護成員。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
Help on class list in module builtins: class list(object) | list() -> new empty list | list(iterable) -> new list initialized from iterable's items | | Methods defined here: | | __add__(self, value, /) | Return self+value. ... | __iter__(self, /) | Implement iter(self). ... | append(...) | L.append(object) -> None -- append object to end | | extend(...) | L.extend(iterable) -> None -- extend list by appending elements from the iterable | | index(...) | L.index(value, [start, [stop]]) -> integer -- return first index of value. | Raises ValueError if the value is not present. ... | pop(...) | L.pop([index]) -> item -- remove and return item at index (default last). | Raises IndexError if list is empty or index is out of range. | | remove(...) | L.remove(value) -> None -- remove first occurrence of value. | Raises ValueError if the value is not present. ... | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __hash__ = None |
在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,這足以解決典型的匯入路徑問題。
按照上述方式,我們的匯入函式會如下所示:
1 2 3 4 5 6 7 8 |
def generatedocs(module): try: sys.path.append(os.getcwd()) # Attempt import mod = safeimport(module) if mod is None: print("Module not found") # Module imported correctly, let's create the docs return getmarkdown(mod) except ErrorDuringImport as e: print("Error while trying to import " + module) |
決定輸出
在繼續之前,你需要一個關於如何組織生成 markdown 輸出的心理影像。思考:你需要一個不遞迴到自定義類的淺的引用嗎?我們想要包含哪些方法?內建功能會怎麼樣?是用_還是__方法?我們應該如何呈現函式簽名?我們應該拉註釋嗎?
我的選擇如下:
- 每個執行一個 .md 檔案,其中包含遞迴到正在檢查的物件的任意子類中生成的資訊。
- 只包括我建立的自定義程式碼,沒有來自匯入的模組的資訊。
- 每一項的輸出必須用第二級 markdown 標題(##)標識。
- 所有標頭檔案必須包含正在描述的項的完整路徑(module.class.subclass.function)。
- 將完整的函式簽名作為預格式化文字。
- 為每個標題提供錨點,以便輕鬆的連結到文件及文件本身內容。
- 任何以_或者__開頭的函式都不做文件記錄。
整合在一起
一旦物件被匯入,我們可以開始檢測了。這是一個簡單的例子,重複呼叫 getmembers(object, filter),過濾器是一個有用的 is 函式。你能夠發現 isclass 和 isfunction,其它相關的方法都是 is開頭的,例如,ismethod, isgenerator, iscoroutine。這都取決於你是否想寫一些通用的,可以處理所有的特殊情況,或一些更小的和更特殊的原始碼。我堅持前兩點,因為我不用把採用3個不同方法來建立我想要的格式化模組,類和功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
def getmarkdown(module): output = [ module_header ] output.extend(getfunctions(module) output.append("***\n") output.extend(getclasses(module)) return "".join(output) def getclasses(item): output = list() for cl in inspect.getmembers(item, inspect.isclass): if cl[0] != "__class__" and not cl[0].startswith("_"): # Consider anything that starts with _ private # and don't document it output.append( class_header ) output.append(cl[0]) # Get the docstring output.append(inspect.getdoc(cl[1]) # Get the functions output.extend(getfunctions(cl[1])) # Recurse into any subclasses output.extend(getclasses(cl[1]) return output def getfunctions(item): for func in inspect.getmembers(item, inspect.isfunction): output.append( function_header ) output.append(func[0]) # Get the signature output.append("\n```python\n) output.append(func[0]) output.append(str(inspect.signature(func[1])) # Get the docstring output.append(inspect.getdoc(func[1]) return output |
當要格式化大文字和一些程式設計程式碼的混合體時,我傾向於把它作為一個在列表或元組中的獨立專案,用 “”.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 內容。
12345678# sofi<a name="sofi"></a><a name="sofi.__init__"></a>### [sofi](#sofi).\_\_init\_\_```python__init__(self)```<a name="sofi.addclass"></a>### [sofi](#sofi).addclass```pythonaddclass(self, selector, cl)```Add the given class from all elements matching this selector.下面是通過 mkdocs 執行它產生 readthedocs 主題頁面後的最終結果(不包括函式註釋)的示例。
我相信你已經知道,使用這些機制自動生成文件,會生成完整、精確和最新的模組資訊,這些資訊在你編寫程式碼的時候可以進行維護和編寫,且操作簡單。我強烈建議每個人都試一試。在結束之前,我想再補充一點,mkdocs 並不是唯一的文件包,還有其他一些使用廣泛的系統,如 Sphinx(mkdocs 基於此開發)和 Doxygen,他們都能實現我們以上討論的事項。然而,我比較通過練習來學習和了解更多關於 Python 內部機制和其隨附的工具。