文档化 Python 代码:完全指南(翻译)

本文最后更新于 2022年3月13日 晚上

原文链接: Documenting Python Code: A Complete Guide - Real Python
作者:James Mertz
译者:muzing

翻译时间:2022.1

允许转载,转载必须保留全部翻译信息,且不能对正文有任何修改

欢迎来到文档化 Python 代码完全指南。不论您是正在文档化一个小脚本还是一个大项目,不论您是初学者还是经验丰富的 Pythonista,这个教程都会涵盖您所需要知道的一切。

译注:原文中 document 为动词,指使得用户阅读代码时有更多更好的文档信息。本文译为“文档化”。

我们将本教程分成了四个主要部分:

  1. 为何文档化代码如此重要 文档化简介、重要性
  2. 注释代码与文档代码 概述注释和文档之间的主要区别,以及恰当使用注释的方法时机
  3. 基于文档字符串文档化Python代码 深入研究类、类方法、函数、模块、包和脚本的文档字符串,以及每个文档字符串中应包含的内容
  4. 文档化Python项目 Python 项目的必要元素及其应包含的内容

自由选择从头至尾通读本教程,或直接跳转至您感兴趣的部分。两种方法皆可。

为何文档化代码如此重要

大概率地,如果您正在阅读本教程,您应该已经知道了文档化代码的重要性。如果还没有,那么让我引用 Guido 在最近的 PyCon 上向我提到的一些内容:

“Code is more often read than written.”

“代码更多被阅读而不是被编写。”

Guido van Rossum (Python 之父)

当您在编写代码时,您主要为两类受众而写:用户与开发者(包括您自己)。二者都同样重要。如果您像我一样,可能已经打开了旧的代码库,询问自己“我当时到底想做什么?”如果您在阅读自己的代码时遇到了困难,想象一下您的用户或者其他开发人员在尝试使用或贡献该代码时的体验。

反过来,我相信您遇到过这样的情况:您想用 Python 做一些事情,并且找到了一个看起来很好的库可以完成该工作。但是,当开始使用该库时,您会查找有关如何执行特定操作的示例、文章甚至官方文档,而不能立即找到解决方案。

一番搜索之后,您发现文档不足,甚至更糟,完全缺失。这种沮丧感让您不再使用该库,不论代码多么出色或高效。Daniele Procida 对这种情况有最好的总结:

“It doesn’t matter how good your software is, because if the documentation is not good enough, people will not use it.

“你的软件有多好并不重要,因为如果文档不够好,人们不会使用它。

Daniele Procida

在本指南中,您将从头开始学习如何正确文档化您的 Python 代码,从最小的脚本到最大的 Python 项目,以防您的用户感到沮丧而无法使用或为您的项目做出贡献。

注释代码与文档代码

在开始讨论如何文档化 Python 代码之前,我们需要区分文档和注释。

一般来说,注释是向/为开发人员描述代码。预期的主要受众是 Python 代码的维护者和开发者。与编写良好的代码相结合,注释有助于引导读者更好地理解代码及其目的与设计:

“Code tells you how; Comments tell you why.”

“代码体现实现方式;注释反映原因。”

Jeff Atwood (即 Coding Horror)

文档化的代码向用户描述其使用和功能。虽然它可能在开发过程中有所帮助,但主要的目标受众是用户。以下部分描述了如何以及何时对代码进行注释。

代码注释基础

在 Python 中使用井号(#)创建注释,并且它应该是不超过几句话的简短陈述。以下是一个简单的例子:

1
2
3
def hello_world():
# 简单的打印语句前的简单注释
print("Hello World")

根据 PEP 8,注释应有 72 个字符的最大长度限制。即使您的项目将最大行长度设置为大于推荐的 80 个字符,这一项依然适用。如果一个注释比该注释字符限制更长,使用多行注释是合适的:

1
2
3
4
def hello_long_world():
# 一条很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长
# 的直到达到 80 个字符的限制才结束的陈述
print("Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooo World")

注释代码包含许多种目的,包括

  • 计划与审查: 当您开发代码的新部分时,首先使用注释作为规划或概述该部分代码的方式可能是合适的。记得在实际编写代码和审查/测试后删除这些注释:

    1
    2
    3
    # 第一步
    # 第二步
    # 第三步
  • 代码描述: 注释可用于解释特定代码段的意图:

    1
    # 尝试基于先前设置的连接。如果不成功,提示用户进行新设置。
  • 算法描述: 当使用算法时,尤其是复杂的算法时,解释算法如何工作或如何在代码中实现会很有用。 描述为什么选择一种特定算法而不是另一种也可能是合适的。

    1
    # 使用快速排序(quick sort)来提高性能
  • 标记: 标记可用于标记代码已知问题,或该位置所在区域的特定代码的改进。例如 BUGFIXME 以及 TODO

    1
    # TODO: 添加 val 为 None 时的条件

对代码的注释应保持简短和集中。尽可能避免使用长注释。此外,您应该按照 Jeff Atwood 的建议使用以下四个基本规则:

  1. 使注释尽可能靠近被描述的代码。不在其描述代码附近的注释会让读者感到沮丧,并且在更新时很容易错过。
  2. 不要使用复杂的格式(例如表格或 ASCII 数字)。复杂的格式会导致内容分散注意力,并且随着时间的推移可能难以维护。
  3. 不要包含多余的信息。假设代码的读者对编程原理和语言语法有基本的了解。
  4. 让代码有自解释性。理解代码最简单的方法是阅读它。当您使用清晰、易于理解的概念设计代码时,读者能够快速对您的意图形成概念。

请记住,注释是为读者(包括您自己)设计的,以帮助引导他们理解软件的目的和设计。

通过 Type Hint 注释代码

译者注:Type Hint 译作“类型提示”,有关内容推荐阅读 Real Python 的另一篇教程 Python Type Checking (Guide)

类型提示已添加到 Python 3.5 中,它是一种附加的格式,可帮助您的代码阅读者。事实上,它把 Jeff 的第四个建议从上面带到了下一个层次。它允许开发者不用注释即可设计和解释他们的部分代码。以下是一个快速示例:

1
2
def hello_name(name: str) -> str:
return(f"Hello {name}")

通过检查类型提示,您可以立即看出该函数期望输入的 namestr 类型。您还可以判断该函数的预期输出也将是 str 类型。虽然类型提示有助于减少注释,但注意在创建或更新项目文档时,这也可能会产生额外的工作量。

您可以从这个由 Dan Bader 创建的视频学习更多类型提示与类型检查的内容。

基于文档字符串文档化 Python 代码

现在我们已经了解了注释,让我们深入研究文档化 Python 代码库。在本节中,您将了解文档字符串以及如何将它们用于文档化。本节进一步分为以下小节:

  1. 文档字符串背景 关于文档字符串如何在 Python 内部工作的背景
  2. 文档字符串类型 各种文档字符串“类型”(函数、类、类方法、模块、包以及脚本)
  3. 文档字符串格式 不同的文档字符串“格式” (Google、NumPy/SciPy、reStructuredText 与 Epytext)

文档字符串背景

文档化您的 Python 代码都以文档字符串为中心。这些内置字符串如果配置正确,可以帮助用户和您自己处理项目的文档。除了文档字符串,Python 还有内置函数 help() ,可将对象的文档字符打印到控制台。下面是一个快速示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> help(str)
Help on class str in module builtins:

class str(object)
| str(object='') -> str
| str(bytes_or_buffer[, encoding[, errors]]) -> str
|
| Create a new string object from the given object. If encoding or
| errors are specified, then the object must expose a data buffer
| that will be decoded using the given encoding and error handler.
| Otherwise, returns the result of object.__str__() (if defined)
| or repr(object).
| encoding defaults to sys.getdefaultencoding().
| errors defaults to 'strict'.
# 为便于阅读而截断

这个输出是如何产生的?由于 Python 中一切皆对象,可以使用 dir() 命令检查对象的目录。让我们尝试一下,看看会发现什么:

1
2
>>> dir(str)
['__add__', ..., '__doc__', ..., 'zfill'] # 为便于阅读而截断

在该目录输出中,有一个有趣的属性 __doc__。如果检查该属性,您会发现:

1
2
3
4
5
6
7
8
9
10
11
>>> print(str.__doc__)
str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or
errors are specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object.__str__() (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to 'strict'.

哇哦!您已经找到文档字符串在对象中的存储位置。这意味着您可以直接操作该属性。然而,内置对象有一些限制:

1
2
3
4
>>> str.__doc__ = "I'm a little string doc! Short and stout; here is my input and print me for my out"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'

任何其他自定义对象都可以被操作:

1
2
3
4
def say_hello(name):
print(f"Hello {name}, is it me you're looking for?")

say_hello.__doc__ = "A simple function that says hello... Richie style"
1
2
3
4
5
>>> help(say_hello)
Help on function say_hello in module __main__:

say_hello(name)
A simple function that says hello... Richie style

Python 还有一项功能可以简化文档字符串的创建。不直接操作 __doc__ 属性,而在对象下方放置的字符串将由于其位置特殊性自动被设置为 __doc__ 属性。以下的例子和上一个例子等效:

1
2
3
def say_hello(name):
"""A simple function that says hello... Richie style"""
print(f"Hello {name}, is it me you're looking for?")
1
2
3
4
5
>>> help(say_hello)
Help on function say_hello in module __main__:

say_hello(name)
A simple function that says hello... Richie style

好耶!您已经了解了文档字符串的背景。现在是时候了解不同类型的文档字符串以及它们应该包含哪些信息。

文档字符串类型

PEP 257 中描述了文档字符串约定。其目的在于为用户提供对象的简要概述。它们应该保持足够简洁以易于维护,但仍要足够详尽以便新用户理解其目的及如何使用已有文档的对象。

在所有情况下,文档字符串都应该使用三重双引号(""")字符串格式。无论文档字符串是否为多行,都应该这样做。最低限度地,文档字符串应该是对以下内容的快速总结,将所有描述内容包含在一行中:

1
"""这是用作对象描述的快速摘要行。"""

多行文档字符串用于在摘要之外进一步详细说明对象。所有多行文档字符串都有以下部分:

  • 一个单行摘要行
  • 摘要后的一个空白行
  • 对文档字符串的进一步阐述
  • 又一个空白行
1
2
3
4
5
6
7
"""这是摘要行

这是对文档字符串的进一步阐述。 在本节中,您可以根据情况进一步详细说明细节。
请注意,摘要和详细说明由一个空白的新行分隔。
"""

# 注意上面的空行。代码应该在本行继续。

所有的文档字符串应有和注释相同的最大长度限制(72 个字符)。文档字符串可以进一步分为三大类:

  • 类文档字符串: 类与类方法
  • 包与模块文档字符串: 包、模块与函数
  • 脚本文档字符串: 脚本和函数

类文档字符串

类文档字符串是为类本身以及任何类方法创建的。 文档字符串紧跟在类或类方法之后,并缩进一级:

1
2
3
4
5
6
7
class SimpleClass:
"""类文档字符串写在这里"""

def say_hello(self, name: str):
"""类方法文档字符串写在这里"""

print(f'Hello {name}')

类文档字符串应包含以下信息:

  • 其目的和行为的简要总结
  • 公共方法,以及简要说明
  • 类属性
  • 如果该类计划被子类化,与子类的接口相关的内容

类构造函数参数应记录在 __init__ 类方法文档字符串中。应使用各自的文档字符串记录各个方法。类文档字符串应包含以下内容:

  • 简要说明该方法是什么以及其用途
  • 传递的参数(必需和可选),包括关键字参数
  • 标记被认为是可选的或具有默认值的参数
  • 执行该方法时发生的副作用
  • 引发的异常
  • 对何时可以调用该方法的限制

举一个代表一个动物的数据类的简单例子。这个类包含一些类属性、实例属性、一个 __init__ 和一个单实例方法

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Animal:
"""
A class used to represent an Animal

...

Attributes
----------
says_str : str
a formatted string to print out what the animal says
name : str
the name of the animal
sound : str
the sound that the animal makes
num_legs : int
the number of legs the animal has (default 4)

Methods
-------
says(sound=None)
Prints the animals name and what sound it makes
"""

says_str = "A {name} says {sound}"

def __init__(self, name, sound, num_legs=4):
"""
Parameters
----------
name : str
The name of the animal
sound : str
The sound the animal makes
num_legs : int, optional
The number of legs the animal (default is 4)
"""

self.name = name
self.sound = sound
self.num_legs = num_legs

def says(self, sound=None):
"""Prints what the animals name is and what sound it makes.

If the argument `sound` isn't passed in, the default Animal
sound is used.

Parameters
----------
sound : str, optional
The sound the animal makes (default is None)

Raises
------
NotImplementedError
If no sound is set for the animal or passed in as a
parameter.
"""

if self.sound is None and sound is None:
raise NotImplementedError("Silent Animals are not supported!")

out_sound = self.sound if sound is None else sound
print(self.says_str.format(name=self.name, sound=out_sound))

包与模块文档字符串

包文档字符串应该放在包的 __init__.py 文件的顶部。此文档字符串应列出包导出的模块和子包。

模块文档字符串类似于类文档字符串。不再记录类和类方法,而是模块和在其中的函数。模块文档字符串应该放在文件的顶部,甚至在 import 之前。模块文档字符串应包括以下内容:

  • 模块及其用途的简要说明
  • 模块导出的任何类、异常、函数和其他对象的列表

模块函数的文档字符串应该包含与类方法相同的项目:

  • 简要说明该函数是什么及其用途
  • 传递的参数(必需和可选),包括关键字参数
  • 标记被视为可选的参数
  • 执行该方法时发生的副作用
  • 引发的异常
  • 对何时可以调用该方法的限制

脚本文档字符串

脚本被认为是从控制台运行的单个可执行文件。脚本的文档字符串位于文件的顶部,并且应该记录得足够好,以便用户能够充分了解如何使用该脚本。当用户传入错误的参数或使用 -h 选项时,它应该可作为其“使用”信息。

如果您在使用 argparse,假如具体参数文档在 argparser.parser.add_argument 函数的 help 参数中被正确记录,那么可以省去它。建议在 argparse.ArgumentParser 的构造函数中使用 __doc__ 作为 description 参数。查看我们关于命令行解析库的教程,了解有关如何使用 argparse 的更多细节,以及其他常见的命令行解析器。

最后,应在文档字符串中列出自定义或第三方导入,以便用户知道运行脚本可能需要哪些包。 下面是一个脚本示例,用于简单地打印出电子表格的列标题:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
"""Spreadsheet Column Printer

This script allows the user to print to the console all columns in the
spreadsheet. It is assumed that the first row of the spreadsheet is the
location of the columns.

This tool accepts comma separated value files (.csv) as well as excel
(.xls, .xlsx) files.

This script requires that `pandas` be installed within the Python
environment you are running this script in.

This file can also be imported as a module and contains the following
functions:

* get_spreadsheet_cols - returns the column headers of the file
* main - the main function of the script
"""

import argparse

import pandas as pd


def get_spreadsheet_cols(file_loc, print_cols=False):
"""Gets and prints the spreadsheet's header columns

Parameters
----------
file_loc : str
The file location of the spreadsheet
print_cols : bool, optional
A flag used to print the columns to the console (default is
False)

Returns
-------
list
a list of strings used that are the header columns
"""

file_data = pd.read_excel(file_loc)
col_headers = list(file_data.columns.values)

if print_cols:
print("\n".join(col_headers))

return col_headers


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'input_file',
type=str,
help="The spreadsheet file to pring the columns of"
)
args = parser.parse_args()
get_spreadsheet_cols(args.input_file, print_cols=True)


if __name__ == "__main__":
main()

文档字符串格式

您可能已经注意到,在本教程给出的整个示例中,都有一些特定的格式和常见元素:ArgumentsReturnsAttributes。某些特定的文档字符串格式有助于 docstring 解析器和用户熟悉了解格式。本教程示例中使用的格式是 NumPy/SciPy 样式的文档字符串。一些最常见的格式如下:

格式种类 描述 Sphynx 支持 正式规范
Google docstrings Google 推荐的文档格式
reStructuredText 官方 Python 文档标准;对初学者不友好但特性丰富
NumPy/SciPy docstrings NumPy 对 reStructuredText 和 Google Docstrings 的结合
Epytext Epydoc 的 Python 改编;非常适合 Java 开发者 非官方

文档字符串格式的选择取决于您,但您应该在整个文档/项目中坚持使用相同的格式。 以下是每种类型的示例,让您了解每种文档格式的样子。

Google 文档字符串例子

1
2
3
4
5
6
7
8
9
10
"""Gets and prints the spreadsheet's header columns

Args:
file_loc (str): The file location of the spreadsheet
print_cols (bool): A flag used to print the columns to the console
(default is False)

Returns:
list: a list of strings representing the header columns
"""

reStructuredText 例子

1
2
3
4
5
6
7
8
9
10
"""Gets and prints the spreadsheet's header columns

:param file_loc: The file location of the spreadsheet
:type file_loc: str
:param print_cols: A flag used to print the columns to the console
(default is False)
:type print_cols: bool
:returns: a list of strings representing the header columns
:rtype: list
"""

NumPy/SciPy 文档字符串例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""Gets and prints the spreadsheet's header columns

Parameters
----------
file_loc : str
The file location of the spreadsheet
print_cols : bool, optional
A flag used to print the columns to the console (default is False)

Returns
-------
list
a list of strings representing the header columns
"""

Epytext 例子

1
2
3
4
5
6
7
8
9
10
"""Gets and prints the spreadsheet's header columns

@type file_loc: str
@param file_loc: The file location of the spreadsheet
@type print_cols: bool
@param print_cols: A flag used to print the columns to the console
(default is False)
@rtype: list
@returns: a list of strings representing the header columns
"""

文档化 Python 项目

Python 项目有各种形状、规模及用途。文档化项目的方式应该适合您的具体情况。牢记该项目的用户是谁,并适应他们的需求。根据项目的类型,建议侧重文档化的某些方面。项目及其文档的一般布局应如下所示:

1
2
3
4
5
6
7
8
project_root/

├── project/ # 项目源代码
├── docs/
├── README
├── HOW_TO_CONTRIBUTE
├── CODE_OF_CONDUCT
├── examples.py

项目通常可以细分为三大类:私有、合作和公共开源。

私人项目

私人项目是仅供个人使用的项目,通常不会与其他用户或开发人员共享。这类项目的文档可能会非常简单。推荐添加到项目中的一些部分如下:

  • Readme: 项目及其目的的简要概述。 包括安装或操作项目的任何特殊要求。
  • examples.py 一个 Python 脚本文件,提供了如何使用该项目的简单示例。

请记住,即使私人项目是为您个人设计的,您也被视为用户。考虑任何可能让您感到困惑的事情,并确保在注释、文档字符串或 readme 文件中收录这些内容。

合作项目

合作项目是您在项目的开发和/或使用中与其他几个人协作的项目。项目的“客户”或用户仍然是您自己和使用该项目的少数人。

文档应该比私人项目更严格一些,主要是为了帮助新成员加入项目或提醒贡献者/用户项目的新变化。推荐添加到项目中的一些部分如下:

  • Readme: 项目及其目的的简要概述。 包括安装或运行项目的任何特殊要求。此外,添加自上一版本以来的任何重大更改。
  • examples.py 一个 Python 脚本文件,提供了如何使用该项目的简单示例。
  • How to Contribute: 这部分应该包括项目的新贡献者如何开始贡献。

公共开源项目

公共和开源项目是旨在与大量用户共享并且可能涉及大型开发团队的项目。这些项目应该将项目文档放在与项目本身的实际开发一样高的优先级。推荐添加到项目中的一些部分如下:

  • Readme: 项目及其目的地简要概述。包括安装或操作项目的任何特殊要求。以及,添加自上一版本以来的所有重大更改。最后,添加指向更多文档、bug 报告和项目的其他重要信息的链接。Dan Bader 编写了一个很棒的教程,介绍了 readme 文件中应包含的所有内容。
  • How to Contribute: 这应该包括项目的新贡献者如何提供帮助。 这包括开发新功能、修复已知问题、添加文档、添加新测试或报告问题。
  • Code of Conduct: 定义其他贡献者在开发或使用您的软件时应该如何对待彼此。这也说明了如果此代码损坏会发生什么。如果您使用 Github,可以使用推荐的措辞生成行为准则模板。特别是对于开源项目,请考虑添加它。
  • License: 描述您的项目正在使用的许可证的纯文本文件。特别是对于开源项目,请考虑添加它。
  • docs: 包含更多文档的文件夹。下一节将更全面地描述应包含的内容以及如何组织此文件夹的内容。

docs 文件夹的四个主要部分

Daniele Procida 发表了精彩的 PyCon 2017 talk 和随后的关于文档化 Python 项目的博客文章。他提到所有项目都应有以下四个主要部分,以助您专注于工作:

  • 教程:指导读者通过一系列步骤完成项目(或有意义的练习)的课程。 面向用户的学习。
  • 操作指南:引导读者完成解决常见问题所需步骤的指南 (问题导向的处方)。
  • 参考:澄清和阐明特定主题的解释。面向理解。
  • 解释说明:技术说明以及如何操作它(关键类、函数、API 等)。类似百科全书文章。

下表显示了所有这些部分如何相互关联以及它们的总体目的:

学习时最有用的 写代码时最有用的
实际步骤 教程 操作指南
理论知识 解释说明 参考

最后,您希望确保您的用户可以访问他们可能遇到的任何问题的答案。 通过以这种方式组织项目,您将能够轻松地以他们能够快速导航的格式回答这些问题。

文档化工具与资源

文档化您的代码,尤其是大型项目,可能会令人生畏。值得庆幸的是,有一些工具和参考可以帮助您入门:

工具 描述
Sphinx 自动生成多种格式文档的工具集合
Epydoc 基于文档字符串为 Python 模块生成 API 文档的工具
Read The Docs 为您自动构建、版本控制和托管您的文档
Doxygen 用于生成支持 Python 以及其他多种语言的文档的工具
MkDocs 一个静态站点生成器,用于帮助使用 Markdown 语言构建项目文档
pycco 一个“quick and dirty”的文档生成器,并排显示代码和文档。查看我们关于如何使用它的教程

除了这些工具外,还有一些额外的教程、视频和文章对您格式化项目非常有用:

  1. Carol Willing - Practical Sphinx - PyCon 2018
  2. Daniele Procida - Documentation-driven development - Lessons from the Django Project - PyCon 2016
  3. Eric Holscher - Documenting your project with Sphinx & Read the Docs - PyCon 2016
  4. Titus Brown, Luiz Irber - Creating, building, testing, and documenting a Python project: a hands-on HOWTO - PyCon 2016
  5. reStructuredText Official Documentation
  6. Sphinx’s reStructuredText Primer

有时,最好的学习方法是模仿他人。 以下是一些很好地实现文档化的项目示例:

从哪里开始?

项目的文档化进程很简单:

  1. 无文档化
  2. 部分文档化
  3. 完整的文档化
  4. 良好的文档化
  5. 极佳的文档化

如果您对文档化的下一步感到茫然,请根据上述的进程查找您项目的当前位置。您有做任何文档化吗?如果没有,那么就从那里开始。如果您已经有一些文档,但是缺少一些关键的项目文件,从添加这些文件开始。

最后,不要因为编写代码所需的大量工作而气馁或不知所措。一旦您开始文档化您的代码,继续下去就会变得容易。


文档化 Python 代码:完全指南(翻译)
https://muzing.top/posts/efe658c0/
作者
Muzing
发布于
2022年1月2日
更新于
2022年3月13日
许可协议