pandas docstring 指南#

关于 docstring 和标准#

Python docstring 是用于文档化 Python 模块、类、函数或方法的字符串,以便程序员无需阅读实现细节就能理解其功能。

此外,从 docstring 自动生成在线 (html) 文档是一种常见做法。Sphinx 就是为此目的服务的。

下面的示例展示了 docstring 的样子

def add(num1, num2):
    """
    Add up two integer numbers.

    This function simply wraps the ``+`` operator, and does not
    do anything interesting, except for illustrating what
    the docstring of a very simple function looks like.

    Parameters
    ----------
    num1 : int
        First number to add.
    num2 : int
        Second number to add.

    Returns
    -------
    int
        The sum of ``num1`` and ``num2``.

    See Also
    --------
    subtract : Subtract one integer from another.

    Examples
    --------
    >>> add(2, 2)
    4
    >>> add(25, 0)
    25
    >>> add(10, -10)
    0
    """
    return num1 + num2

存在一些关于 docstring 的标准,这使得它们更易于阅读,并且可以轻松导出到其他格式,如 html 或 pdf。

每个 Python docstring 应遵循的首要约定定义在 PEP-257 中。

由于 PEP-257 范围较广,也存在其他更具体的标准。对于 pandas 而言,遵循的是 NumPy docstring 约定。这些约定将在本文档中进行解释

numpydoc 是一个支持 NumPy docstring 约定的 Sphinx 扩展。

该标准使用 reStructuredText (reST)。reStructuredText 是一种标记语言,允许在纯文本文件中编码样式。reStructuredText 的文档可在以下位置找到

pandas 提供了一些辅助工具,用于在相关类之间共享 docstring,详见共享 docstring

本文档的其余部分将总结上述所有指南,并提供 pandas 项目特有的补充约定。

编写 docstring#

通用规则#

Docstring 必须使用三个双引号定义。在 docstring 前后不应留有空行。文本从开头引号后的下一行开始。结尾引号单独占据一行(意味着它们不在最后一句话的末尾)。

在极少数情况下,docstring 中会使用粗体或斜体等 reST 样式,但常见的是内联代码,它们呈现在反引号之间。以下被视为内联代码

  • 参数名称

  • Python 代码、模块、函数、内置对象、类型、字面值……(例如 os, list, numpy.abs, datetime.date, True

  • 一个 pandas 类(形式为 :class:`pandas.Series`

  • 一个 pandas 方法(形式为 :meth:`pandas.Series.sum`

  • 一个 pandas 函数(形式为 :func:`pandas.to_datetime`

注意

要仅显示链接类、方法或函数的最后一个组件,请在其前面加上 ~。例如,:class:`~pandas.Series` 将链接到 pandas.Series,但仅显示最后一部分,即 Series 作为链接文本。详见Sphinx 交叉引用语法

def add_values(arr):
    """
    Add the values in ``arr``.

    This is equivalent to Python ``sum`` of :meth:`pandas.Series.sum`.

    Some sections are omitted here for simplicity.
    """
    return sum(arr)

不好

def func():

    """Some function.

    With several mistakes in the docstring.

    It has a blank like after the signature ``def func():``.

    The text 'Some function' should go in the line after the
    opening quotes of the docstring, not in the same line.

    There is a blank line between the docstring and the first line
    of code ``foo = 1``.

    The closing quotes should be in the next line, not in this one."""

    foo = 1
    bar = 2
    return foo + bar

第一部分:简短摘要#

简短摘要是一个句子,以简洁的方式表达了函数的功能。

简短摘要必须以大写字母开头,以句号结尾,并适合放在一行中。它需要表达对象的功能,但不提供细节。对于函数和方法,简短摘要必须以动词不定式开头。

def astype(dtype):
    """
    Cast Series type.

    This section will provide further details.
    """
    pass

不好

def astype(dtype):
    """
    Casts Series type.

    Verb in third-person of the present simple, should be infinitive.
    """
    pass
def astype(dtype):
    """
    Method to cast Series type.

    Does not start with verb.
    """
    pass
def astype(dtype):
    """
    Cast Series type

    Missing dot at the end.
    """
    pass
def astype(dtype):
    """
    Cast Series type from its current type to the new type defined in
    the parameter dtype.

    Summary is too verbose and doesn't fit in a single line.
    """
    pass

第二部分:扩展摘要#

扩展摘要提供了关于函数功能的详细信息。它不应该深入参数细节,也不讨论实现注意事项,这些内容应放在其他部分。

在简短摘要和扩展摘要之间留一个空行。扩展摘要中的每个段落都以句号结尾。

如果函数不是过于通用,扩展摘要应提供关于该函数为何有用及其使用场景的详细信息。

def unstack():
    """
    Pivot a row index to columns.

    When using a MultiIndex, a level can be pivoted so each value in
    the index becomes a column. This is especially useful when a subindex
    is repeated for the main index, and data is easier to visualize as a
    pivot table.

    The index level will be automatically removed from the index when added
    as columns.
    """
    pass

第三部分:参数#

参数的详细信息将在此部分添加。本节的标题为“Parameters”(参数),其后紧跟着一行,该行在“Parameters”每个字母下方都有一个连字符。在章节标题前留一个空行,但标题后以及带有“Parameters”的行与带有连字符的行之间不留空行。

标题之后,签名中的每个参数都必须文档化,包括 *args**kwargs,但不包括 self

参数通过其名称定义,后跟一个空格、一个冒号、另一个空格以及类型(或多种类型)。请注意,名称和冒号之间的空格很重要。不对 *args**kwargs 定义类型,但必须对所有其他参数定义类型。在参数定义之后,需要有一行包含参数描述,该描述应缩进,并可以有多行。描述必须以大写字母开头,以句号结尾。

对于带有默认值的关键字参数,默认值将在类型末尾的逗号后列出。在这种情况下,类型的确切形式将是“int, default 0”。在某些情况下,解释默认参数的含义可能很有用,可以在逗号后添加,例如“int, default -1, meaning all cpus”(表示所有 CPU)。

如果默认值是 None,意味着该值将不被使用。这种情况下,推荐写 "str, optional">`,而不是 "str, default None"。当 None 是一个被使用的值时,我们将保留“str, default None”的形式。例如,在 df.to_csv(compression=None) 中,None 不是一个被使用的值,而是表示压缩是可选的,如果没有提供则不使用压缩。在这种情况下,我们将使用 "str, optional">`。只有在像 func(value=None) 这样 None 被用作与 0foo 相同的方式时,我们才会指定“str, int or None, default None”。

class Series:
    def plot(self, kind, color='blue', **kwargs):
        """
        Generate a plot.

        Render the data in the Series as a matplotlib plot of the
        specified kind.

        Parameters
        ----------
        kind : str
            Kind of matplotlib plot.
        color : str, default 'blue'
            Color name or rgb code.
        **kwargs
            These parameters will be passed to the matplotlib plotting
            function.
        """
        pass

不好

class Series:
    def plot(self, kind, **kwargs):
        """
        Generate a plot.

        Render the data in the Series as a matplotlib plot of the
        specified kind.

        Note the blank line between the parameters title and the first
        parameter. Also, note that after the name of the parameter ``kind``
        and before the colon, a space is missing.

        Also, note that the parameter descriptions do not start with a
        capital letter, and do not finish with a dot.

        Finally, the ``**kwargs`` parameter is missing.

        Parameters
        ----------

        kind: str
            kind of matplotlib plot
        """
        pass

参数类型#

指定参数类型时,可以直接使用 Python 内置数据类型(优先使用 Python 类型,而不是更详细的 string、integer、boolean 等)

  • int

  • float

  • str

  • bool

对于复杂类型,定义子类型。对于 dicttuple,由于存在多种类型,我们使用括号来帮助阅读类型(dict 使用花括号,tuple 使用圆括号)

  • list of int

  • dict of {str : int}

  • tuple of (str, int, int)

  • tuple of (str,)

  • set of str

如果只允许一组特定的值,则将它们列在花括号中,并用逗号分隔(后跟一个空格)。如果这些值是有序的,则按此顺序排列。否则,如果存在默认值,则先列出默认值

  • {0, 10, 25}

  • {‘simple’, ‘advanced’}

  • {‘low’, ‘medium’, ‘high’}

  • {‘cat’, ‘dog’, ‘bird’}

如果类型定义在 Python 模块中,必须指定模块名

  • datetime.date

  • datetime.datetime

  • decimal.Decimal

如果类型在某个包中,也必须指定模块名

  • numpy.ndarray

  • scipy.sparse.coo_matrix

如果类型是 pandas 类型,除了 Series 和 DataFrame 外,也必须指定 pandas

  • Series

  • DataFrame

  • pandas.Index

  • pandas.Categorical

  • pandas.arrays.SparseArray

如果精确类型不重要,但必须与 NumPy 数组兼容,则可以指定 array-like。如果接受任何可迭代类型,则可以使用 iterable

  • array-like

  • iterable

如果接受多种类型,用逗号分隔,除了最后两种类型,它们需要用单词‘or’分隔

  • int or float

  • float, decimal.Decimal or None

  • str or list of str

如果 None 是接受的值之一,它必须始终是列表中的最后一个。

对于 axis,约定使用类似以下格式

  • axis : {0 or ‘index’, 1 or ‘columns’, None}, default None

第四部分:返回或生成#

如果方法返回一个值,将在本节中进行文档化。如果方法生成其输出,也是如此。

本节的标题将以与“Parameters”相同的方式定义。使用名称“Returns”或“Yields”,其后跟着一行,该行包含与前面单词字母数相同的连字符。

返回值的文档编写方式也类似于参数。但在这种情况下,不提供名称,除非方法返回或生成多个值(值的元组)。

“Returns”和“Yields”的类型与“Parameters”的类型相同。此外,描述必须以句号结尾。

例如,单个返回值

def sample():
    """
    Generate and return a random number.

    The value is sampled from a continuous uniform distribution between
    0 and 1.

    Returns
    -------
    float
        Random number generated.
    """
    return np.random.random()

多个返回值

import string

def random_letters():
    """
    Generate and return a sequence of random letters.

    The length of the returned string is also random, and is also
    returned.

    Returns
    -------
    length : int
        Length of the returned string.
    letters : str
        String of random letters.
    """
    length = np.random.randint(1, 10)
    letters = ''.join(np.random.choice(string.ascii_lowercase)
                      for i in range(length))
    return length, letters

如果方法生成其值

def sample_values():
    """
    Generate an infinite sequence of random numbers.

    The values are sampled from a continuous uniform distribution between
    0 and 1.

    Yields
    ------
    float
        Random number generated.
    """
    while True:
        yield np.random.random()

第五部分:参见#

本节用于告知用户与当前文档化的功能相关的 pandas 功能。在极少数情况下,如果找不到任何相关的方法或函数,则可以跳过本节。

一个明显的例子是 head()tail() 方法。由于 tail() 执行与 head() 相同的功能,但作用于 SeriesDataFrame 的末尾而不是开头,因此告知用户这一情况是很好的。

为了直观地了解哪些可以被视为相关内容,这里有一些例子

  • lociloc,因为它们的功能相同,但一个提供索引,另一个提供位置

  • maxmin,因为它们功能相反

  • iterrows, itertuplesitems,因为用户查找遍历列的方法时很容易找到遍历行的方法,反之亦然

  • fillnadropna,因为这两种方法都用于处理缺失值

  • read_csvto_csv,因为它们是互补的

  • mergejoin,因为前者是后者的泛化

  • astypepandas.to_datetime,因为用户可能正在阅读 astype 的文档以了解如何转换为日期类型,而实现该功能的正确方法是使用 pandas.to_datetime

  • wherenumpy.where 相关,因为其功能基于后者

在决定哪些内容相关时,应主要运用常识,并思考对于阅读文档的用户,特别是经验较少的用户,什么信息会有用。

关联其他库(主要是 numpy)时,先使用模块名(而非像 np 这样的别名)。如果函数位于非主模块中,如 scipy.sparse,则列出完整的模块名(例如 scipy.sparse.coo_matrix)。

本节有一个标题,“See Also”(请注意 S 和 A 大写),其后跟着一行连字符,前面有一个空行。

标题之后,我们将为每个相关的方法或函数添加一行,后跟一个空格、一个冒号、另一个空格以及一个简短描述,该描述阐明了该方法或函数的功能、在此上下文中为何相关,以及当前文档化的函数与引用的函数之间的主要区别。描述也必须以句号结尾。

请注意,在“Returns”和“Yields”部分,描述位于类型之后的下一行。但在本节中,描述位于同一行,中间用冒号分隔。如果描述无法容纳在同一行,可以继续到其他行,这些行必须进一步缩进。

例如

class Series:
    def head(self):
        """
        Return the first 5 elements of the Series.

        This function is mainly useful to preview the values of the
        Series without displaying the whole of it.

        Returns
        -------
        Series
            Subset of the original series with the 5 first values.

        See Also
        --------
        Series.tail : Return the last 5 elements of the Series.
        Series.iloc : Return a slice of the elements in the Series,
            which can also be used to return the first or last n.
        """
        return self.iloc[:5]

第六部分:注意事项#

这是一个可选部分,用于说明算法实现方面的注意事项,或文档化函数行为的技术细节。

除非您熟悉算法实现,或者在为函数编写示例时发现了一些反直觉的行为,否则可以随意跳过本节。

本节遵循与扩展摘要部分相同的格式。

第七部分:示例#

这是 docstring 中最重要的部分之一,尽管放在最后面,因为人们通常通过示例比通过准确的解释更能理解概念。

docstring 中的示例除了说明函数或方法的用法外,必须是有效的 Python 代码,能够以确定性方式返回给定输出,并且用户可以复制和运行。

示例以 Python 终端会话的形式呈现。>>> 用于表示代码。... 用于表示代码承接上一行。输出紧随生成输出的最后一行代码之后呈现(中间没有空行)。可以在示例描述注释的前后添加空行。

呈现示例的方式如下

  1. 导入所需的库(numpypandas 除外)

  2. 创建示例所需的数据

  3. 展示一个非常基础的示例,以说明最常见的使用场景

  4. 添加带有解释的示例,说明如何使用参数实现扩展功能

一个简单的示例如下

class Series:

    def head(self, n=5):
        """
        Return the first elements of the Series.

        This function is mainly useful to preview the values of the
        Series without displaying all of it.

        Parameters
        ----------
        n : int
            Number of values to return.

        Return
        ------
        pandas.Series
            Subset of the original series with the n first values.

        See Also
        --------
        tail : Return the last n elements of the Series.

        Examples
        --------
        >>> ser = pd.Series(['Ant', 'Bear', 'Cow', 'Dog', 'Falcon',
        ...                'Lion', 'Monkey', 'Rabbit', 'Zebra'])
        >>> ser.head()
        0   Ant
        1   Bear
        2   Cow
        3   Dog
        4   Falcon
        dtype: object

        With the ``n`` parameter, we can change the number of returned rows:

        >>> ser.head(n=3)
        0   Ant
        1   Bear
        2   Cow
        dtype: object
        """
        return self.iloc[:n]

示例应尽可能简洁。如果函数复杂度要求较长的示例,建议使用带有粗体标题的块。使用双星号 ** 使文本加粗,例如 **this example**

示例约定#

示例中的代码假定总是以以下两行未显示的代码开头

import numpy as np
import pandas as pd

示例中使用的任何其他模块都必须显式导入,每行一个(如 PEP 8#imports 中推荐),并避免使用别名。避免过多的导入,但如果需要,标准库的导入放在前面,其次是第三方库(如 matplotlib)。

当使用单个 Series 示例时,使用名称 ser;如果使用单个 DataFrame 示例时,使用名称 df。对于索引,首选名称是 idx。如果使用一组同质的 SeriesDataFrame,则命名为 ser1, ser2, ser3… 或 df1, df2, df3…。如果数据非同质且需要多种结构,则使用有意义的名称,例如 df_maindf_to_join

示例中使用的数据应尽可能紧凑。建议行数约为 4,但应根据具体示例选择合适的数字。例如,在 head 方法中,需要大于 5 行才能展示默认值的示例。如果是计算 mean,可以使用 [1, 2, 3] 这样的数据,这样很容易看出返回的值是平均值。

对于更复杂的示例(例如分组),避免使用没有解释的数据,例如列为 A, B, C, D… 的随机数矩阵。相反,使用有意义的示例,这样更容易理解概念。除非示例要求,否则使用动物名称及其数值属性,以保持示例的一致性。

调用方法时,优先使用关键字参数 head(n=3),而不是位置参数 head(3)

class Series:

    def mean(self):
        """
        Compute the mean of the input.

        Examples
        --------
        >>> ser = pd.Series([1, 2, 3])
        >>> ser.mean()
        2
        """
        pass


    def fillna(self, value):
        """
        Replace missing values by ``value``.

        Examples
        --------
        >>> ser = pd.Series([1, np.nan, 3])
        >>> ser.fillna(0)
        [1, 0, 3]
        """
        pass

    def groupby_mean(self):
        """
        Group by index and return mean.

        Examples
        --------
        >>> ser = pd.Series([380., 370., 24., 26],
        ...               name='max_speed',
        ...               index=['falcon', 'falcon', 'parrot', 'parrot'])
        >>> ser.groupby_mean()
        index
        falcon    375.0
        parrot     25.0
        Name: max_speed, dtype: float64
        """
        pass

    def contains(self, pattern, case_sensitive=True, na=numpy.nan):
        """
        Return whether each value contains ``pattern``.

        In this case, we are illustrating how to use sections, even
        if the example is simple enough and does not require them.

        Examples
        --------
        >>> ser = pd.Series('Antelope', 'Lion', 'Zebra', np.nan)
        >>> ser.contains(pattern='a')
        0    False
        1    False
        2     True
        3      NaN
        dtype: bool

        **Case sensitivity**

        With ``case_sensitive`` set to ``False`` we can match ``a`` with both
        ``a`` and ``A``:

        >>> s.contains(pattern='a', case_sensitive=False)
        0     True
        1    False
        2     True
        3      NaN
        dtype: bool

        **Missing values**

        We can fill missing values in the output using the ``na`` parameter:

        >>> ser.contains(pattern='a', na=False)
        0    False
        1    False
        2     True
        3    False
        dtype: bool
        """
        pass

不好

def method(foo=None, bar=None):
    """
    A sample DataFrame method.

    Do not import NumPy and pandas.

    Try to use meaningful data, when it makes the example easier
    to understand.

    Try to avoid positional arguments like in ``df.method(1)``. They
    can be all right if previously defined with a meaningful name,
    like in ``present_value(interest_rate)``, but avoid them otherwise.

    When presenting the behavior with different parameters, do not place
    all the calls one next to the other. Instead, add a short sentence
    explaining what the example shows.

    Examples
    --------
    >>> import numpy as np
    >>> import pandas as pd
    >>> df = pd.DataFrame(np.random.randn(3, 3),
    ...                   columns=('a', 'b', 'c'))
    >>> df.method(1)
    21
    >>> df.method(bar=14)
    123
    """
    pass

使您的示例通过 doctest 的技巧#

让示例通过验证脚本中的 doctest 有时可能很棘手。这里有一些注意事项

  • 导入所有需要的库(pandas 和 NumPy 除外,它们已导入为 import pandas as pdimport numpy as np),并定义示例中使用的所有变量。

  • 尽量避免使用随机数据。但在某些情况下使用随机数据可能是可以接受的,例如如果您文档化的函数涉及概率分布,或者使函数结果有意义所需的数据量太大,手动创建非常麻烦。在这些情况下,始终使用固定的随机种子,使生成的示例可预测。示例

    >>> np.random.seed(42)
    >>> df = pd.DataFrame({'normal': np.random.normal(100, 5, 20)})
    
  • 如果您的代码片段跨越多行,需要在续行上使用‘…’

    >>> df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], index=['a', 'b', 'c'],
    ...                   columns=['A', 'B'])
    
  • 如果要展示引发异常的情况,可以这样写

    >>> pd.to_datetime(["712-01-01"])
    Traceback (most recent call last):
    OutOfBoundsDatetime: Out of bounds nanosecond timestamp: 712-01-01 00:00:00
    

    必须包含“Traceback (most recent call last):”,但对于实际错误,只需要错误名称即可。

  • 如果结果中有小部分可能变化(例如对象表示中的哈希值),可以使用 ... 表示这部分。

    如果您想展示 s.plot() 返回一个 matplotlib AxesSubplot 对象,这会使 doctest 失败

    >>> s.plot()
    <matplotlib.axes._subplots.AxesSubplot at 0x7efd0c0b0690>
    

    但是,您可以这样做(注意需要添加的注释)

    >>> s.plot()  
    <matplotlib.axes._subplots.AxesSubplot at ...>
    

示例中的图表#

pandas 中有一些方法返回图表。为了在文档中渲染示例生成的图表,存在 .. plot:: 指令。

要使用它,请在“Examples”标题后放置以下代码,如下所示。构建文档时将自动生成图表。

class Series:
    def plot(self):
        """
        Generate a plot with the ``Series`` data.

        Examples
        --------

        .. plot::
            :context: close-figs

            >>> ser = pd.Series([1, 2, 3])
            >>> ser.plot()
        """
        pass

共享 docstring#

pandas 有一个在类之间共享 docstring 的系统,允许细微变化。这有助于我们保持 docstring 的一致性,同时保持对用户阅读的清晰。这会增加编写时的一些复杂性。

每个共享的 docstring 都有一个带有变量的基础模板,例如 {klass}。变量随后使用 doc 装饰器填充。最后,还可以使用 doc 装饰器向 docstring 追加内容。

在这个示例中,我们将正常创建一个父 docstring(类似于 pandas.core.generic.NDFrame)。然后我们将有两个子类(类似于 pandas.core.series.Seriespandas.core.frame.DataFrame)。我们将在此 docstring 中替换类名。

class Parent:
    @doc(klass="Parent")
    def my_function(self):
        """Apply my function to {klass}."""
        ...


class ChildA(Parent):
    @doc(Parent.my_function, klass="ChildA")
    def my_function(self):
        ...


class ChildB(Parent):
    @doc(Parent.my_function, klass="ChildB")
    def my_function(self):
        ...

结果 docstring 如下

>>> print(Parent.my_function.__doc__)
Apply my function to Parent.
>>> print(ChildA.my_function.__doc__)
Apply my function to ChildA.
>>> print(ChildB.my_function.__doc__)
Apply my function to ChildB.

注意

  1. 我们将父 docstring“追加”到最初为空的子 docstring 中。

我们的文件中通常包含一个模块级别的 _shared_doc_kwargs,其中包含一些常见的替换值(例如 klass, axes 等)。

您可以通过类似以下方式一次性完成替换和追加

@doc(template, **_shared_doc_kwargs)
def my_function(self):
    ...

其中 template 可能来自模块级别的 _shared_docs 字典,该字典将函数名称映射到 docstring。在可能的情况下,我们首选使用 doc,因为这样编写 docstring 的过程更接近正常方式。

请参见 pandas.core.generic.NDFrame.fillna 获取模板示例,以及 pandas.core.series.Series.fillnapandas.core.generic.frame.fillna 获取填充后的版本。