🐍

【Python】__repr__ と __str__ を深掘る

2024/11/18に公開

問題提起

まず以下のコードをご覧ください。

from datetime import datetime, timezone

epoch_start = datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc)

print(epoch_start)
print([epoch_start])

出力は以下のようになります。

1970-01-01 00:00:00+00:00
[datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)]

epoch_start を単に print した場合とリストに包んで print した場合とで、何やら違いがあるようです。この挙動は初見だと意外なのではないでしょうか。

Python には、オブジェクトの基本的な振る舞いを定めるために言語レベルでサポートされた特殊メソッドという概念があります。初期化の方法を定める __init__ が代表的です。

文字列表現もオブジェクトの基本的な振る舞いの一つですが、それを定めるための特殊メソッドは二種類存在します。__repr____str__ です。リンク先のドキュメントに説明が書かれていますが、要は __repr__ による文字列表現は基本的には eval に通すことで等価なオブジェクトを復元できることが期待されていて、__str__ は特にそういう期待がなく人間が読みやすいように設定できる、といったところです。単に print をした場合は __str__ による文字列表現が出力されるということなので、先の挙動について前者は説明がつきますが、後者は __repr__ による文字列表現のように見え、そうなる理由は自明ではありません。

本記事では、なぜそうなるのかを仕様レベルで確認したのち、なぜそういう仕様になっているのかの調査結果を共有します。さらに、それに基づいて、__repr____str__ をどのように定義すればいいかを考察します。

なぜそうなるのか

実はリストの __str__ は明示的に定義されていません。じゃあどうなっているのかというと、__repr__ は定義されていて、それが勝手に呼ばれます。__str__ のデフォルト実装が __repr__ を呼ぶようになっていることは、先ほどのドキュメントに記載があります。そしてリストの __repr__ は各要素の __repr__ を呼びます。

リストの __repr__ の挙動自体には何の不思議もありません。では、同様に __str__ も各要素の __str__ を呼んでいるのかなと考えたくなるわけですが、そうはなっていないのです。

Python のコンテナや dataclass は、おしなべてこういう感じになっています。

なぜそういう仕様になっているのか

実をいうと、そんなに多くを語れるだけのものを見つけられてはいないのですが、重要だと思われるのは PEP 3140 です。コンテナの __str__ では各要素の __str__ を呼ぶべきだという提言で、これが却下されています。

時期的には Python 3 がリリースされる少し前のもので、ベータ版に近い今これを受け入れては混乱を引き起こすだろう、とだけ却下理由が述べられています。

一方で提言者自身が述べているデメリットは以下の通りです。

  • コンテナの __str__ がどういう文字列を出力すべきかはそもそも難しい。
  • __repr__ の方が情報がリッチであることが期待できる。

後者については、リッチな情報が欲しいのであればコンテナの __repr__ を叩けばいい気もしますが、前者は結構重要なのではないでしょうか。コンテナは各要素の __str__ による文字列表現がどのようになっているかは知り得ないわけで、常に適切な包み方や区切り方というのは存在しなさそうです。

却下したのは「優しい終身の独裁者」こと Guido 氏ですが、本当にベータ版に近かったからというだけのことなのか、こうしたデメリットを重く見た結果なのか、何か他に深い考えがあったのかはわかりません。

__repr____str__ をどのように定義すればいいか

以上の話は、我々が __repr____str__ をどのように定義すればいいかということについても示唆を与えています。まず言えるのは、__str__ の階層的呼び出しには実態不明のオブジェクトが挟まってはならないということです。

また、簡潔な文字列表現の定義には __str__ を使いたくなるものですが、コンテナや dataclass にラップされたままに中身を確認したいこともあるわけです。この問題は割と難しくて、dataclass であれば __str__ を自分で実装してもいいですが、面倒ですし、コンテナの場合はそれもできません。一方、__repr__ の方のドキュメントには、eval で復元できる文字列表現が返せない場合には <...some useful description...> という形式で返していいと書かれています。これを拡大解釈して、使いたくなったら積極的に使ってしまうのもアリかもしれません。

まとめ

Python の __repr____str__ について深掘りました。もしかしたら、いうほど深掘れてはいなくて若干消化不良かもしれませんが、何か知見がある方はぜひ教えてください。

mutex Official Tech Blog

Discussion