Python の join() が文字列型のメソッドである理由

Python で区切り文字を使って文字列を連結する join() は文字列型のメソッドです。

''.join(['松', '竹', '梅'])  # => '松竹梅'
' | '.join(['Home', 'About', 'Services'])  # => 'Home | About | Services'

他の言語では join() は配列( Python でいうリスト)のメソッドとして用意されているケースが多いため、他のプログラミング言語を知った後に Python を学んだ人の多くがこの「 join() が文字列型のメソッドであること」に気持ち悪さを感じるようです。

なぜ Python の join() はリストではなく文字列型のメソッドとして用意されているのでしょうか。今回はこのあたりを見てみたいと思います。

経緯

文字列型の join() メソッドは 2000 年頃( Python の最新バージョンが 1.6 の頃)に、 string モジュール内の関数 string.join(seq, delimiter) と同等の機能を持つものとして導入されました(ちなみに string.join() は削除されたため現在は存在しません)。

興味のある方は Python 1.6 のリリースノートをご覧になってみてください。

join() メソッド追加の際の議論は「 String methods... finally 」というタイトルのスレッドで行われたようで、その全容は今もアーカイブ上で確認することができます。

「文字列型の join() メソッド」に落ち着くまでにさまざまな選択肢が議論されたようで、そのあたりのことが次の Stack Overflow の質問への回答の中でわかりやすくまとめられています。

ここの回答によると、文字列を連結できる機能の候補として以下の 4 つがあげられたとのこと。

  • 文字列型のメソッド str.join(seq)
  • シーケンスのメソッド seq.join(str)
  • シーケンスのメソッド seq.reduce(str)
  • 関数 join(seq, delimiter)

ここで、 str は文字列を表すオブジェクト、 seq は sequence/iterable オブジェクトを意味します。

このうち、 seq.reduce(str) はわかりづらいという理由でボツとなり、関数 join(seq, delimiter) は文字列型との結びつきが強い機能なのにグローバルな名前空間を使うのはよくないという理由で候補から外れたようです。

となると残るは str.join(seq)seq.join(str) の 2 つですが、この 2 つの間では次のような理由で str.join(seq) の方に軍配が上がったようです。

  • str.join(seq) なら文字列やリストやタプルなど組み込みの型だけでなく、後から追加されるあらゆる sequence/iterable オブジェクトに自動で対応できる。
  • seq.join(str)seq の要素がすべて文字列型のときにだけ使えるメソッドであり、それが seq 一般のメソッドになっているのは変。
  • 区切り文字は省略できない形の方がよいが、 seq.join(str) では省略できるように見えてしまう。

その他、 seq の各要素を自動的に文字列型にキャストすべきかどうかといった議論もそのときになされたようです(結論は「キャストはしない」ということになっています)。

メリット / デメリット

上で見た導入時の議論ポイントも含めて、 join() が文字列型のメソッドであることのメリット / デメリットをまとめてみます。

メリット

  • 文字列型やリストやタプルだけでなく、後から追加される sequence/iterable なオブジェクトに自動で対応できる。
  • Python は sequence/iterable を扱う方法として、 sequence/iterable そのものにメソッドを追加するのではなく、 map()filter()sum()functools.reduce() などのように sequence/iterable を受け取る関数を用意していることが多い。 str.join(seq) にすれば、それらと一貫性がある。
  • sep.join(seq) は、「 separator joins sequence 」という順番になっているので英語としてむしろ自然な並びである。
  • join() の処理の中身はあくまでも文字列の連結なので、文字列型のメソッドになっている方がまとまりがよい。
  • 関数の join(sep, seq) なら引数の順番で迷う可能性があるが、 str.join(seq) なら迷わない。

デメリット

  • 他の言語の join() の使い方に慣れ親しんだ人にとってわかりづらい。

これぐらいでしょうか。このように挙げてみると、デメリットは慣れの問題だけで、総合して考えるとメリットの方が断然多いような気がしてきます。

中でも決め手になるのは Python には文字列を表すための型が複数ある点だと思います。もし仮に Python で文字列を表すための型が 1 つしかなかったなら seq.join(str) という書き方もありといえばありだった気もしますが、文字列を表すための型が複数ある状況では、 sequence/iterable な各クラスの方で毎回文字列型に対応させるためのプロトコルのようなものを実装するのはあまりよいやり方とはいえなさそうです。

# 複数の文字列型で共通の形で join が利用できる

# 1. str
sep = '☆'
seq = ('サマー', 'キャンペーン')
sep.join(seq)  # => b'サマー☆キャンペーン'

# 2. bytes
sep = bytes('☆', 'sjis')
seq = (bytes('サマー', 'sjis'), bytes('キャンペーン', 'sjis'))
sep.join(seq)  # => b'\x83T\x83}\x81[\x81\x99\x83L\x83\x83\x83\x93\x83y\x81[\x83\x93'

# 3. bytearray
sep = bytearray('☆', 'utf-8')
seq = (bytearray('サマー', 'utf-8'), bytearray('キャンペーン', 'utf-8'))
sep.join(seq)  # => bytearray(b'\xe3\x82\xb5\xe3\x83\x9e\xe3\x83\xbc\xe2\x98\x86\xe3\x82\xad\xe3\x83\xa3\xe3\x83\xb3\xe3\x83\x9a\xe3\x83\xbc\xe3\x83\xb3')

以上です。

なお、上であげた理由などはあくまでも各種リソースをもとにした私の解釈です。正確なニュアンスを知りたい方は直接原典にあたるなどしてみていただければと思います。

参考