阅读代码比编写代码更多,有良好丰富文档的项目会吸引更多人使用和参与开发贡献。本教程旨在详细阐述如何将 Python 代码实现“文档化”,介绍了注释用法、类型提示、文档字符串、在项目中组织文档、工具推荐等内容,对初学者或 Pythonista 均适用。
本文翻译自 Documenting Python Code: A Complete Guide - Real Python
作者 James Mertz
译者 muzing
转载必须保留以上全部信息(含链接)
本译文首发于我的博客,欢迎移步获得更好的阅读体验,或查看我的更多技术文章:
文档化 Python 代码:完全指南(翻译) - muzing 的杂货铺
欢迎来到文档化 Python 代码完全指南。不论您是正在文档化一个小脚本还是一个大项目,不论您是初学者还是经验丰富的 Pythonista,这个教程都会涵盖您所需要知道的一切。
译注:原文中 document 为动词,指使得用户阅读代码时有更多更好的文档信息。本文译为“文档化”。
我们将本教程分成了四个主要部分:
自由选择从头至尾通读本教程,或直接跳转至您感兴趣的部分。两种方法皆可。
大概率地,如果您正在阅读本教程,您应该已经知道了文档化代码的重要性。如果还没有,那么让我引用 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 中使用井号(#
)创建注释,并且它应该是不超过几句话的简短陈述。以下是一个简单的例子:
def hello_world():
# 简单的打印语句前的简单注释
print("Hello World")
根据 PEP 8,注释应有 72 个字符的最大长度限制。即使您的项目将最大行长度设置为大于推荐的 80 个字符,这一项依然适用。如果一个注释比该注释字符限制更长,使用多行注释是合适的:
def hello_long_world():
# 一条很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长
# 的直到达到 80 个字符的限制才结束的陈述
print("Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooo World")
注释代码包含许多种目的,包括:
计划与审查: 当您开发代码的新部分时,首先使用注释作为规划或概述该部分代码的方式可能是合适的。记得在实际编写代码和审查/测试后删除这些注释:
# 第一步
# 第二步
# 第三步
代码描述: 注释可用于解释特定代码段的意图:
# 尝试基于先前设置的连接。如果不成功,提示用户进行新设置。
算法描述: 当使用算法时,尤其是复杂的算法时,解释算法如何工作或如何在代码中实现会很有用。 描述为什么选择一种特定算法而不是另一种也可能是合适的。
# 使用快速排序(quick sort)来提高性能
标记: 标记可用于标记代码已知问题,或该位置所在区域的特定代码的改进。例如 BUG
、FIXME
以及 TODO
。
# TODO: 添加 val 为 None 时的条件
对代码的注释应保持简短和集中。尽可能避免使用长注释。此外,您应该按照 Jeff Atwood 的建议使用以下四个基本规则:
请记住,注释是为读者(包括您自己)设计的,以帮助引导他们理解软件的目的和设计。
译者注:Type Hint 译作“类型提示”,有关内容推荐阅读 Real Python 的另一篇教程 Python Type Checking (Guide)
类型提示已添加到 Python 3.5 中,它是一种附加的格式,可帮助您的代码阅读者。事实上,它把 Jeff 的第四个建议从上面带到了下一个层次。它允许开发者不用注释即可设计和解释他们的部分代码。以下是一个快速示例:
def hello_name(name: str) -> str:
return(f"Hello {name}")
通过检查类型提示,您可以立即看出该函数期望输入的 name
是 str
类型。您还可以判断该函数的预期输出也将是 str
类型。虽然类型提示有助于减少注释,但注意在创建或更新项目文档时,这也可能会产生额外的工作量。
您可以从这个由 Dan Bader 创建的视频学习更多类型提示与类型检查的内容。
现在我们已经了解了注释,让我们深入研究文档化 Python 代码库。在本节中,您将了解文档字符串以及如何将它们用于文档化。本节进一步分为以下小节:
文档化您的 Python 代码都以文档字符串为中心。这些内置字符串如果配置正确,可以帮助用户和您自己处理项目的文档。除了文档字符串,Python 还有内置函数 help()
,可将对象的文档字符打印到控制台。下面是一个快速示例:
>>> 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()
命令检查对象的目录。让我们尝试一下,看看会发现什么:
>>> dir(str)
['__add__', ..., '__doc__', ..., 'zfill'] # 为便于阅读而截断
在该目录输出中,有一个有趣的属性 __doc__
。如果检查该属性,您会发现:
>>> 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'.
哇哦!您已经找到文档字符串在对象中的存储位置。这意味着您可以直接操作该属性。然而,内置对象有一些限制:
>>> 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 "" , line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'
任何其他自定义对象都可以被操作:
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"
>>> 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__
属性。以下的例子和上一个例子等效:
def say_hello(name):
"""A simple function that says hello... Richie style"""
print(f"Hello {name}, is it me you're looking for?")
>>> help(say_hello)
Help on function say_hello in module __main__:
say_hello(name)
A simple function that says hello... Richie style
好耶!您已经了解了文档字符串的背景。现在是时候了解不同类型的文档字符串以及它们应该包含哪些信息。
PEP 257 中描述了文档字符串约定。其目的在于为用户提供对象的简要概述。它们应该保持足够简洁以易于维护,但仍要足够详尽以便新用户理解其目的及如何使用已有文档的对象。
在所有情况下,文档字符串都应该使用三重双引号("""
)字符串格式。无论文档字符串是否为多行,都应该这样做。最低限度地,文档字符串应该是对以下内容的快速总结,将所有描述内容包含在一行中:
"""这是用作对象描述的快速摘要行。"""
多行文档字符串用于在摘要之外进一步详细说明对象。所有多行文档字符串都有以下部分:
"""这是摘要行
这是对文档字符串的进一步阐述。 在本节中,您可以根据情况进一步详细说明细节。
请注意,摘要和详细说明由一个空白的新行分隔。
"""
# 注意上面的空行。代码应该在本行继续。
所有的文档字符串应有和注释相同的最大长度限制(72 个字符)。文档字符串可以进一步分为三大类:
类文档字符串是为类本身以及任何类方法创建的。 文档字符串紧跟在类或类方法之后,并缩进一级:
class SimpleClass:
"""类文档字符串写在这里"""
def say_hello(self, name: str):
"""类方法文档字符串写在这里"""
print(f'Hello {name}')
类文档字符串应包含以下信息:
类构造函数参数应记录在 __init__
类方法文档字符串中。应使用各自的文档字符串记录各个方法。类文档字符串应包含以下内容:
举一个代表一个动物的数据类的简单例子。这个类包含一些类属性、实例属性、一个 __init__
和一个单实例方法:
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
的更多细节,以及其他常见的命令行解析器。
最后,应在文档字符串中列出自定义或第三方导入,以便用户知道运行脚本可能需要哪些包。 下面是一个脚本示例,用于简单地打印出电子表格的列标题:
"""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()
您可能已经注意到,在本教程给出的整个示例中,都有一些特定的格式和常见元素:Arguments
、Returns
和 Attributes
。某些特定的文档字符串格式有助于 docstring 解析器和用户熟悉了解格式。本教程示例中使用的格式是 NumPy/SciPy 样式的文档字符串。一些最常见的格式如下:
格式种类 | 描述 | Sphynx 支持 | 正式规范 |
---|---|---|---|
Google docstrings | Google 推荐的文档格式 | 是 | 否 |
reStructuredText | 官方 Python 文档标准;对初学者不友好但特性丰富 | 是 | 是 |
NumPy/SciPy docstrings | NumPy 对 reStructuredText 和 Google Docstrings 的结合 | 是 | 是 |
Epytext | Epydoc 的 Python 改编;非常适合 Java 开发者 | 非官方 | 是 |
文档字符串格式的选择取决于您,但您应该在整个文档/项目中坚持使用相同的格式。 以下是每种类型的示例,让您了解每种文档格式的样子。
"""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
"""
"""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
"""
"""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
"""
"""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 项目有各种形状、规模及用途。文档化项目的方式应该适合您的具体情况。牢记该项目的用户是谁,并适应他们的需求。根据项目的类型,建议侧重文档化的某些方面。项目及其文档的一般布局应如下所示:
project_root/
│
├── project/ # 项目源代码
├── docs/
├── README
├── HOW_TO_CONTRIBUTE
├── CODE_OF_CONDUCT
├── examples.py
项目通常可以细分为三大类:私有、合作和公共开源。
私人项目是仅供个人使用的项目,通常不会与其他用户或开发人员共享。这类项目的文档可能会非常简单。推荐添加到项目中的一些部分如下:
examples.py
: 一个 Python 脚本文件,提供了如何使用该项目的简单示例。请记住,即使私人项目是为您个人设计的,您也被视为用户。考虑任何可能让您感到困惑的事情,并确保在注释、文档字符串或 readme 文件中收录这些内容。
合作项目是您在项目的开发和/或使用中与其他几个人协作的项目。项目的“客户”或用户仍然是您自己和使用该项目的少数人。
文档应该比私人项目更严格一些,主要是为了帮助新成员加入项目或提醒贡献者/用户项目的新变化。推荐添加到项目中的一些部分如下:
examples.py
: 一个 Python 脚本文件,提供了如何使用该项目的简单示例。公共和开源项目是旨在与大量用户共享并且可能涉及大型开发团队的项目。这些项目应该将项目文档放在与项目本身的实际开发一样高的优先级。推荐添加到项目中的一些部分如下:
docs
文件夹的四个主要部分Daniele Procida 发表了精彩的 PyCon 2017 talk 和随后的关于文档化 Python 项目的博客文章。他提到所有项目都应有以下四个主要部分,以助您专注于工作:
下表显示了所有这些部分如何相互关联以及它们的总体目的:
学习时最有用的 | 写代码时最有用的 | |
---|---|---|
实际步骤 | 教程 | 操作指南 |
理论知识 | 解释说明 | 参考 |
最后,您希望确保您的用户可以访问他们可能遇到的任何问题的答案。 通过以这种方式组织项目,您将能够轻松地以他们能够快速导航的格式回答这些问题。
文档化您的代码,尤其是大型项目,可能会令人生畏。值得庆幸的是,有一些工具和参考可以帮助您入门:
工具 | 描述 |
---|---|
Sphinx | 自动生成多种格式文档的工具集合 |
Epydoc | 基于文档字符串为 Python 模块生成 API 文档的工具 |
Read The Docs | 为您自动构建、版本控制和托管您的文档 |
Doxygen | 用于生成支持 Python 以及其他多种语言的文档的工具 |
MkDocs | 一个静态站点生成器,用于帮助使用 Markdown 语言构建项目文档 |
pycco | 一个“quick and dirty”的文档生成器,并排显示代码和文档。查看我们关于如何使用它的教程。 |
除了这些工具外,还有一些额外的教程、视频和文章对您格式化项目非常有用:
有时,最好的学习方法是模仿他人。 以下是一些很好地实现文档化的项目示例:
项目的文档化进程很简单:
如果您对文档化的下一步感到茫然,请根据上述的进程查找您项目的当前位置。您有做任何文档化吗?如果没有,那么就从那里开始。如果您已经有一些文档,但是缺少一些关键的项目文件,从添加这些文件开始。
最后,不要因为编写代码所需的大量工作而气馁或不知所措。一旦您开始文档化您的代码,继续下去就会变得容易。