docstringsを使って、パッケージのAPIのコードを文書化する。#

APIとは何か?#

API は Applied Programming Interface の略です。 (Pythonの)パッケージの文脈で議論される場合、APIはパッケージメンテナがユーザーのために作成する関数、クラス、メソッド、属性を指します。

パッケージAPI要素の単純な例: 例えば、あるパッケージに add_numbers() という関数があり、その関数はたくさんの数字を足し算するとします。 数字を足し算するには、ユーザは add_numbers(1,2,3) を呼び出すだけで、パッケージの関数が値を計算して 6 を返します。 add_numbers 関数を呼び出すことで、パッケージのAPIを使用していることになります。

パッケージAPIは、ユーザーインターフェイスを作成する関数、クラス、メソッド、属性から構成されます。

docstringとは何ですか?docstringとドキュメンテーションの関係は?#

Pythonにおいてdocstringとは、関数、メソッド、クラスにおいて、その関数が何を行うのか、その入出力を記述したテキストを指します。 Pythonプログラマは通常、関数への入力を "parameters" または "arguments" と呼び、出力を "return values" と呼ぶことが多いです。

docstringは以下のために重要です:

  • Pythonで help() を呼び出すと、例えば help(add_numbers) は関数のdocstringのテキストを表示します。 このようにdocstringは、ユーザが自分のワークフローにより効果的に関数を適用する方法をよりよく理解するのに役立ちます。

  • パッケージのドキュメントを作成するとき、docstringsを使用して、すべての関数、クラス、メソッド、属性をきれいに表示する完全なAPIドキュメントを自動的に作成することもできます。

PythonパッケージAPIドキュメント#

もし、あなたのパッケージのすべてのユーザー向けクラス、メソッド、属性、関数に説明的なdocstringがあれば(合理的な範囲内で)、そのパッケージのAPIは十分に文書化されているとみなされます。

Pythonの場合、これは、パッケージ内のすべてのユーザー向けのクラス、メソッド、属性、および/または関数について、 (無理のない範囲で) docstringを追加する必要があることを意味します:

  • 関数、メソッド、属性、またはクラスが何をするのかを説明します

  • 入力と出力の type を定義します( stringintnp.array など)

  • オブジェクト、メソッド、関数の期待される出力 return を説明する。

Pythonの3つのdocstringフォーマットとNumPyスタイルが好きな理由#

Pythonのdocstringフォーマットには、パッケージを文書化する際に使用できるものがいくつかあります:

Pythonのドキュメントには NumPy-style docstrings を使うことをお勧めします:

  • NumPyスタイルのdocstringは、科学的なPythonエコシステムの中核であり、 NumPyスタイルガイド で定義されています。 そのため、NumPyスタイルガイドで広く使われています。

  • Numpyスタイルのドキュメントストリングは単純化されているので、コード内でもPythonで help() を呼び出すときでも読みやすくなっています。 対照的に、reSTスタイルのdocstringは素早くスキャンするのが難しく、モジュールの中でより多くの行を占める可能性があると感じる人もいます。

Tip

もしNumPy形式のドキュメントを使用しているのであれば、ドキュメントの conf.py ファイルに sphinx napoleon extension を含めるようにしてください。 この拡張機能により、SphinxはNumPy形式のドキュメントを正しく読み、フォーマットすることができます。

Docstringの例 Better と Best#

以下はよく文書化された関数の良い例です。 この関数のdocstringには、関数の入力と出力(または戻り値)が記述されていることに注目してください。 関数の最初の説明は短いです(1行)。 その1行の説明に続いて、関数が何をするのかについて少し長い説明があります(2~3文)。 関数の戻り値も指定されています。

def extent_to_json(ext_obj):
    """Convert bounds to a shapely geojson like spatial object.

    This format is what shapely uses. The output object can be used
    to crop a raster image.

    Parameters
    ----------
    ext_obj : list or geopandas.GeoDataFrame
        If provided with a `geopandas.GeoDataFrame`, the extent
        will be generated from that. Otherwise, extent values
        should be in the order: minx, miny, maxx, maxy.

    Returns
    -------
    extent_json: A GeoJSON style dictionary of corner coordinates
    for the extent
        A GeoJSON style dictionary of corner coordinates representing
        the spatial extent of the provided spatial object.
    """

ベスト: 関数の使用例を記載したdocstring#

この例には、sphinxでも doctest を使ってテストされている関数の使用例が含まれています。

def extent_to_json(ext_obj):
    """Convert bounds to a shapely geojson like spatial object.

    This format is what shapely uses. The output object can be used
    to crop a raster image.

    Parameters
    ----------
    ext_obj : list or geopandas.GeoDataFrame
        If provided with a `geopandas.GeoDataFrame`, the extent
        will be generated from that. Otherwise, extent values
        should be in the order: minx, miny, maxx, maxy.

    Returns
    -------
    extent_json : A GeoJSON style dictionary of corner coordinates
    for the extent
        A GeoJSON style dictionary of corner coordinates representing
        the spatial extent of the provided spatial object.

    Example
    -------
    Convert a `geopandas.GeoDataFrame` to an extent dictionary:

    >>> import geopandas as gpd
    >>> import earthpy.spatial as es
    >>> from earthpy.io import path_to_example

	We start by loading a Shapefile.

    >>> rmnp = gpd.read_file(path_to_example('rmnp.shp'))

	And then use `extent_to_json` to do the conversion from `shp` to
    `geopandas.GeoDataFrame`.

    >>> es.extent_to_json(rmnp)
    {'type': 'Polygon', 'coordinates': (((-105.4935937, 40.1580827), ...),)}

    """
../../_images/sphinx-rendering-extent-to-json-earthpy.png

上記のNumPyフォーマットのdocstringをsphinxで使用すると、autodocエクステンションが extent_to_json 関数のabout documentationセクションを作成します。 es.extent_to_json(rmnp) コマンドの出力は、doctestを使ってテストすることもできます。#

doctestを使って、パッケージのメソッドや関数でdocstringのサンプルを実行します。#

上記では、良いdocstringフォーマット、より良いdocstringフォーマット、ベストなdocstringフォーマットの例をいくつか紹介しました。 もしSphinxを使ってドキュメントを作成しているのであれば、 doctest という拡張機能をSphinxビルドに追加することができます。Doctestは、サンプルコードを含むdocstringに対して追加のチェックを行います。 Doctest は、docstring Examples 内のサンプルコードを実行し、期待される出力が正しいかどうかをチェックします。 ドキュメントのチュートリアルを実行するのと同様に、 doctest はパッケージのコード (API) が期待通りに動作することを保証する便利なステップになります。

注釈

It's important to keep in mind that examples in your docstrings help users using your package. Running doctest on those examples provides a check of your package's API. The doctest ensures that the functions and methods in your package run as you expect them to. Neither of these items replace a separate, stand-alone test suite that is designed to test your package's core functionality across operating systems and Python versions.

doctestは以下の例を実行し、 add_me に1と3の値を与えると4が返されることをテストします。

def add_me(num1, num2):
    """A function that sums two numbers.

    Parameters
    ----------
    num1 : int
        An integer value to be added
    num2 : int
        An integer value to be added

    Returns
    -------
       The integer sum of the provided numbers.

    Examples
    --------
    Below you can see how the `add_me` function will return a number.

    >>> add_me(1, 3)
    4

    """

    return num1 + num2


docstringに型ヒントを追加する#

上の例では、パラメータとして関数に渡されるデータ型や、属性としてクラスに渡されるデータ型を記述するために、numpyスタイルのdocstringを使用することを見ました。 numpyスタイルのdocstringでは、docstringのParametersセクションにそれらのデータ型を追加します。 下の例では、パラメータ num1num2 は両方とも Python の int (整数) 値であるべきであることがわかります。

    Parameters
    ----------
    num1 : int
        An integer value to be added
    num2 : int
        An integer value to be added

関数やメソッドが必要とするデータ型を記述することで、ユーザーが関数やメソッドの呼び出し方をより理解しやすくなります。

型ヒントは、あなたのコードに型ドキュメントの別のレイヤーを追加します。 型ヒントは、新しい開発者や将来の自分、あるいは貢献者が、あなたのコードベースについて素早く知ることを容易にします。

型のヒントは関数の定義に追加されます。 以下の例では、パラメータaNumとaNum2はtype = int(整数)として定義されています。

def add_me(num1: int, num2: int):
    """A function that sums two numbers.

さらに -> を使って、期待される関数の出力を記述することができます。以下の関数の出力も int です。

def add_me(num1: int, num2: int) -> int:
    """A function that sums two numbers.

型ヒントを使う理由#

型ヒント:

  • 開発とデバッグをより速くします、

  • メソッドや関数のデータ・フォーマットの入出力を、ユーザーが見やすくします、

  • 型が正しいことを確認するためにコードをチェックする mypy のような静的型チェックツールの使用をサポートします。

あなたのコードにタイプヒンティングを追加することを検討すべきです:

  • あなたのパッケージはデータ処理を行います、

  • 複雑な入力を必要とする関数を使用しています

  • あなたは、あなたのコードを手助けしてくれる新しい貢献者の入り口の障壁を低くしたいです。

型ヒントの多用に注意してください

コードに型ヒントを追加する際には、場合によってはこのようなこともあることを考慮してほしい:

  • 複雑なコードベースを持っている場合、型ヒントはコードを読みにくくする可能性がある。 パラメーターの入力が複数のデータ型を取り、それぞれをリストアップする場合は特にそうだ。

  • 単純なスクリプトや明らかな操作を行う関数に型ヒントを書いても意味がありません。

型ヒントを徐々に追加#

型ヒントの追加には多くの時間がかかります。 しかし、コードを書きながら少しずつ型ヒントを追加していくことができます。

Tip

型ヒントを追加することは、新しい貢献者にとっても素晴らしい作業です。 より複雑な貢献をする前に、あなたのパッケージのコードと構造をよりよく知ることができます。