🥶

Docstringより型エイリアスを書こう

に公開

I hate docstring

極論MonsterのYosematです。
私はDocstringが大嫌いです。その理由は...いろんなところに書かれているから。

def process_odd_number(n: int) -> None:
    """
    Processes the odd number.

    Args:
        n: Must be odd.
    """
    if n % 2 == 0:
        raise ValueError("n must be odd.")
    # ... do something with n ...


def update_odd_number(n: int) -> None:
    """
    Updates stats based on the odd number.

    Args:
        n: Must be odd.
    """
    if n % 2 == 0:
        raise ValueError("n must be odd.")

プログラミングを上達する方法は?と聞かれれば、皆さん矢継ぎ早にたくさんのアイデアが出てくるかと思いますが、1つだけこだわるなら私はいつも決まって「Don’t Repeat Yourself (DRY)」—ソフトウェア開発の基本原則を徹底して守ることを勧めています。そしてその舌の根も乾かぬうちに同じdocstringを様々な関数にコピーペーストするのです…。DRYを破った代償は皆さんもご存知の通り。管理されなくなったOutdatedなドキュメントの乱立です。

今日私が主張したいのはDocstringより型エイリアスを書こうです。

Docstringより型エイリアスを書こう

どうしてdocstringがいろんな関数やクラスに乱立するのでしょうか?
原因のひとつは、「本来は型に集約できる説明」を関数側に書いてしまうことにあります。

上の例では「nという整数型の引数は奇数でなければならない」という要件が、process_odd_numberやupdate_odd_numberのdocstringに記載されています。このように関数の使い方をよりわかりやすく表現するために開発者たちは変数名docstringを編集してきました。私が提案したいのは新しい型を作ることです。

エイリアスで作る

型を作るシンプルな方法は新たなclass定義を書くことですが、今日推したいのは「別名(エイリアス)」として型を定義する方法です。
例えば以下のように**「OddNumberはintの一種だ」と示しつつ型名とコメントで“奇数である”旨を書いておく**だけで一目瞭然な型として機能するではありませんか。

from typing import TypeAlias

#: OddNumber — int だけど「奇数」を表したい
OddNumber: TypeAlias = int

もちろん、これだけでは実行時に「奇数かどうか」の検証は行いません。でも、型名だけでも「ただの int と違う特別な意味がある」ことを明示できます。何よりこの型名は複数の定義で使いまわすことができます

def process_odd_number(n: OddNumber) -> None:
    print(f"Processing odd number: {n}")

def update_odd_number(n: OddNumber) -> None:
    print(f"Updating stats with odd number: {n}")

これで、関数docstringに「このnは奇数で…」と書かなくても、型エイリアスがすべて語ってくれます。
引数を呼び出すほうも「OddNumber なら奇数なんだな」とすぐわかる。

もっと厳密にやりたいならPydanticのValidator

Pydantic v2では、PlainValidator という新機能を使って関数のバリデーションロジックを型エイリアスに直接組み込むことができます。以下の例では OddNumber という型を定義し、「値が奇数である」ことを検証します。

from typing import Annotated
from pydantic import PlainValidator

def odd_validator(value: int) -> int:
    """奇数であることを保証するバリデーション"""
    if value % 2 == 0:
        raise ValueError("This value must be an odd number.")
    return value

#: OddNumber — int型だけど、奇数のみが許される
OddNumber = Annotated[int, PlainValidator(odd_validator)]

Note: 通常Pydanticではバリデーションはモデル定義にしか行われませんが@validate_callを通じて通常の関数にもバリデーションをかけることができます。

from pydantic import validate_call

@validate_call
def process_odd_number(n: OddNumber) -> None:
    # docstringを省略しても、nが奇数であることは型名で分かるし
    # Pydanticで自動チェックもされる
    print(f"Processing odd number: {n}")

自前でValidatorを実装する前にPydantic Typesに既に必要な機能がそろっていないか目を通しておくとよいでしょう。

Docstringを頼ってもいい

ここまでdocstringを目の敵にしてきましたが本質的に関数に紐づくドキュメントだってあるし認めたくはないけどカーソルを載せたら定義が表示されるIDEサポートはやっぱりクールです。複雑すぎる情報は短い型名で語ることもできません。
良いニュースなのはPEP727 - Documentation in Annotated Metadataで型名に紐づくDocstringとそのエディターサポートが議論されていることです。まだDraftPEPですが今後の進捗を見守りましょう。

最後に

私は「Docstringより型エイリアスを書こう」というアイデアに強い自信をもっています。それでも、開発現場で使うときは周囲をよく見渡したほうがいいかもしれません。極論モンスターは責任能力がございませんので。

今日もOpinionatedな記事を書いてみました。皆さんも引き続き快適なPython Lifeを!Yosematでした。

Discussion