在我刚开始接触Python的日子里,我最喜欢做的事情之一是坐在解释器旁使用内置help功能来检查类和方法,决定下一个要敲的内容。这个功能导入一个对象,遍布它的成员,取出文档注释,生成一个类似manpage的输出,从而帮助你找到如何使用正在检查的对象的方法。 它被内置成一个标准库的美妙之处在于通过代码直接生成输出,它为我这样的懒人间接地强调了一个编码风格,我就想着在尽量少做额外的工作的情况下维护文档。尤其是如果你已经为你的变量和函数选择直接的名字。 这种风格涉及到向你的函数和类添加文档字符串,以及通过用下划线前缀来正确地识别私有成员和受保护成员。
在Python解释器上运行help(list)的输出。 |
帮助功能实际上是使用 pydoc 模块生成其输出,它也可以从命令行运行,以产生您路径中任何可导入模块的文本或 html 表示。 不久以前,我需要编写更细致,更正式的设计文档。而作为一个 Markdown 的粉丝,我决定玩一玩mkdocs,看看是否能获得我想要的结果。这个模块可以很容易地将你的 markdown 文档转化为一个样式美观的页面,并且,在发布为官方文档之前,允许你对其做更改。它提供了一个 readthedocs 模板,深圳还提供了一个简单的命令行界面,来把你的修改推送到 GitHub Pages 里面。 在完成了我最初的描述设计决策和注意事项的文本后,我想加上一些细节,描述我正在开发的实际接口方法。因为我已经为大部分的功能写了定义,我想从源代码自动生成参考页面,还希望这个能在 markdown 里面。这样的话,只要我想跑 mkdocs,它就可以连同我文档的其余部分,一起渲染成 html。 |
然而,事实上还没有一种从源码生成 markdown 的默认方式,除了一些插件。后来,我不断在谷歌上查询,我还是不满意我发现的插件——很多东西都过时了,没有人维护了,或者输出的东西不是我需要的——因此,我决定写一个我自己的项目。我认为这很有意思,这也让我学到了更多关于构建和调试一个模块的知识,更多内容可以查看我之前的文章( 设计一个简单的图形 Python 调试器):inspect 模块。
|
检查!Inspect,源自于标准程序库,它不仅允许你查看较低级别的 python 框架和代码对象,它还提供很多方法来检查模块和类,帮你发现可能感兴趣的的项目。这个也就是之前提到的用来生成帮助文件的 pydoc。 浏览一下在线文档,你会发现许多跟我们所做的尝试相关的方法。最重要的几个是getmembers(),getdoc() 和 signature(),还有许多给 getmembers 做滤波器的 is... 功能。拥有这些,我们可以轻易地循环访问很多功能,包括区分生成器和协同程序,并可以按需要递归到任何一个类以及内部。 |
导入编码 如果我们要去检查一个对象,不管它是什么,第一步要做的是提供一个导入进我们的命名空间的原理。为何要讨论导入呢?这取决于你想要做什么,还有很多需要担心的,包括虚拟环境,自定义代码,标准模块和重命名。情况会容易混淆,搞错的话会需要一些时间去整理清楚。 我们当然还有些选择,更复杂的是直接从pydoc重用safeimport(),当出现问题时,为我们解决很多特例和ErrorDuringImport类的特别条款。然而,如果我们对我们的环境需要更高的控制,我们自己简单地运行__import__(modulename)也是可能的。 |
另一个需要记住的是每一个代码的执行路径。可能会用到 sys.path.append() 的一个目录来进入我们寻找的模块。我的用例我的用例是从命令行和被检查的模块的路径中的目录执行,所以,我将当前目录添加到 sys.path,这足以解决典型的导入路径问题。 按照上述方式,我们的导入函数会如下所示:
|
决定输出在继续之前,你需要一个关于如何组织生成 markdown 输出的心理图像。思考:你需要一个不递归到自定义类的浅的引用吗?我们想要包含哪些方法?内置功能会怎么样?是用_还是__方法?我们应该如何呈现函数签名?我们应该拉注释吗? 我的选择如下:
|
整合在一起
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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 outputdef 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 对象 ,它表示其位置参数和关键字参数的默认值和任何注释,为我们提供了产生简单直接的描述和良好风格的文本,有助于我们理解用户我们写代码的意图。
其他考虑因素和非预期后果
|
示例输出我在 sofi 包-精确的说是 sofi.app 模块运行生成器,下面是它创建的 markdown 内容。
下面是通过 mkdocs 运行它产生 readthedocs 主题页面后的最终结果(不包括函数注释)的示例。 我相信你已经知道,使用这些机制自动生成文档,会生成完整、精确和最新的模块信息,这些信息在你编写代码的时候可以进行维护和编写,且操作简单。我强烈建议每个人都试一试。 在结束之前,我想再补充一点,mkdocs 并不是唯一的文档包,还有其他一些使用广泛的系统,如 Sphinx(mkdocs 基于此开发)和 Doxygen,他们都能实现我们以上讨论的事项。然而,我比较通过练习来学习和了解更多关于 Python 内部机制和其随附的工具。 |
本文标题:如何编写 Python 文档生成器
本文地址:https://www.oschina.net/translate/python-introspection-with-the-inspect-module
参与翻译:爱不爱吃鱼, wnull, 无若, spache
英文原文:How to write your own Python documentation generator