【Python】doctestで説明文のコード例が古くなるのを防ぐ

PythonのTopに戻る

説明文の実行例も自動で確かめる

関数を修正したのにdocstringの使用例を直し忘れると、説明どおりに試しても同じ結果にならない。doctestを使うと、docstringに書いた>>>から始まる短い対話例を実行し、その次に書いた期待結果と比較できる。使い方を伝える例を、そのまま簡単な回帰確認として利用できるのが利点である。

doctestは標準ライブラリなので、この例に追加パッケージは不要である。説明の短い例を保守する用途に向き、複雑な準備、大量の境界値、外部依存の差し替えなどはpytestの通常のテストへ分けると読みやすい。文書として伝えたい使い方と、網羅的に検査したい条件を無理に一つのdocstringへ詰め込まない。

戻り値・空行・例外をdocstringに書く

labels.py

def compact_label(text):
    """Normalize whitespace in a short label.

    >>> compact_label(" sample   A ")
    'sample A'
    >>> print(compact_label(""))
    <BLANKLINE>
    >>> compact_label(None)
    Traceback (most recent call last):
        ...
    TypeError: text must be str
    """
    if not isinstance(text, str):
        raise TypeError("text must be str")
    return " ".join(text.split())

labels.pyとして保存し、python -m doctest -v labels.pyで実行する。-vを付けると、何を試し、何を期待しているかも表示される。ここでは3つの例が成功した。次は実行結果の末尾部分である。

実行結果(抜粋)

3 passed and 0 failed.
Test passed.

説明だけ古くなった例を実際に失敗させる

broken_doc.py

def twice(value):
    """Double a value.

    >>> twice(3)
    7
    """
    return value * 2

こちらは説明文の期待値7が誤っている。python -m doctest broken_doc.pyを実行すると失敗し、終了コードは1になる。下は実際の失敗表示から、ExpectedとGotの部分を抜粋したもの。関数は6を返しているので、仕様が「2倍」であることを確認して期待値を6へ直す。

実行結果(抜粋)

Expected:
    7
Got:
    6

表示されるものを正確に書く

対話環境で式を評価した戻り値はrepr()に相当する表記で表示されるため、文字列の例には引用符が付く。一方、print()の結果には通常その引用符が付かない。コードが同じ文字列を扱っていても、何を表示しているかによって期待出力は変わる。例の最初は戻り値、二つ目はprintの表示である。

期待結果中の空行は<BLANKLINE>と書く。単なる空行を置くと例の区切りとして扱われるため、出力された空行を明示する必要がある。複数行のコードは…で継続行を書くが、これは任意の結果を省略するための指定とは別である。対話形式の記号と結果を混ぜないようにする。

例外を期待する場合はTracebackの行と例外の種類・メッセージを書く。途中のスタックのファイル名や行番号は例の本質ではないので、例のように省略できる。ここではNoneを渡したときTypeErrorになることを確認し、正しい入力だけを説明するdocstringより契約が分かりやすくなる。

再現できない出力を持ち込まない

現在時刻、一時ファイルの絶対パス、ランダムな値、集合の表示順、外部サービスの結果などは実行環境で変わる。説明に必要なら値を固定したり、並び順をsorted()で整えたりしてから表示する。実行ごとに変わる値をそのまま期待結果へ貼り付けると、関数に問題がなくても失敗し続ける。

ELLIPSISやNORMALIZE_WHITESPACEなどのオプションで比較を緩めることもできるが、必要な箇所に限る。出力の重要な違いまで無視すると、説明が古くなったことを検知できなくなる。浮動小数点なら表示桁数を決めた例にするなど、読者にも意味の分かる形で安定させるのがよい。

文書の検査もコードの実行である

doctestは書かれた例を実行するので、他者が作った任意の文書を無条件に検査してよいわけではない。ファイルを消す例や通信する例があれば、その副作用も起きうる。自分が管理する例を、限定したデータで実行する。モジュールのimport時に処理が走る場合もあるため、関数の定義と実行入口を分けておく。

成功したlabels.pyの3例と、意図的に失敗するbroken_doc.pyの1例は別々に実行した。全部の例が成功したわけではない。普段の文書検査へ組み込む際は誤った期待値を修正し、コード変更と説明変更を同じ機会に確認する。短い使い方が正しく保たれるだけでも、後から関数を使う人の手戻りを減らせる。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:pytestで正常系と異常系の自動テストを始める / pytestのparametrizeで境界値と入力パターンをまとめて検証する

PythonのTopに戻る