【Python】__repr__ と __str__ を深掘る
問題提起
まず以下のコードをご覧ください。
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__
について深掘りました。もしかしたら、いうほど深掘れてはいなくて若干消化不良かもしれませんが、何か知見がある方はぜひ教えてください。
Discussion