2012年09月30日

Pythonはなぜ?str.join(seq)なのか?

Screen Shot 2012-09-29 at 10.13.21 AM

PythonのAPI設計の中で、たまに思い出したように話題が出てくるのが、配列に入った文字列を結合するメソッド。Pythonではstr.join(iterable)です。他の言語(僕がよく知っているRubyとJavaScript)はArray.join(String)となっています。どちらでもありえる話ですが、個人的にはPythonの方が自然だな、と感じていました。ですが、他の言語の方がいいという人も多く、Pythonプログラマーの中でも好き嫌いが出たりもします。せっかく、弾さんがPerlの国からやってきて適度にガソリンをまいて炎上したところなので、Pythonの歴史を紐解いてみました。

軽くjoinの歴史について語っているサイトはないか探してみる

軽くぐぐってみると、何箇所か言及しているところがありました。

まずは無料のPython教育資料のDive Into Python。1.14. Joining lists and splitting stringsの項目の中に、歴史に関するメモ(Historical Note)という欄があります。ここによると:

  • 1.6以前のPythonの文字列には便利なメソッドがなかった(実際、配列に毛が生えた程度)
  • 文字列操作はstringというモジュールをimportして行っていた
  • 2.0で文字列のメソッドが強化された時に他のモジュール関数と一緒にstring.joinも文字列のメソッドになった
  • 当時はハードコアなPythonプログラマでもこれに反対(配列に入れるべき)の人がたくさんいた

他には、Stack Over FlowのPython join, why is it string.join(list) instead of list.join(string)?というスレッドもひっかかりましたが、この中で技術的に語っているのもありました。

  • リストやタプル、ジェネレータなど、いろんなシーケンスに対応する必要がある
  • 文字列、バイト列など、いろいろな対象があり、対象ごとに処理が変わる

それに対して、それならシーケンスの共通親クラスから対象のjoinを呼べばいいだけやんか!とツッコミが入っています。まあこのツッコミでは足りないのですが、元の回答も足りないのは確かです。どちらの回答者も、逆転裁判をプレイして論理思考を訓練すべきですね。

補足

@atsuoishimotoさんより、Pythonは2.2まではCで書かれた組み込みクラスの継承はできなかったとのコメントがありました。たしかにそうだった。

これは多重ディスパッチと呼ばれる問題の1つです。2種類のクラス群が関係しています。シーケンスのクラスがn個あって、文字列の種類がm個あれば、n×m個の組み合わせがあります。大抵の場合は、nとmのうち、変化の少ない方、あるいは数が少ない方のメソッドとして実装する、あるいはまったく属さないで独立の関数として実装する方法が取られますが、まあどちらに属させても正しいかどうかは状況による、といった感じです。後者の場合はパターンマッチ(関数型言語)やオーバーロード(C++)などの言語機能とセットになりますが、Pythonなどの動的言語にはそれらの言語機能がないので、前者の実装となります。

いろいろ見てみましたが、状況が分かっただけで、「なぜPythonがこうしたのか?」の手がかりはまだ得られていません。ここは本丸を攻めるしかありません。敵はpython.orgにあり!

PEPを見てみる

Pythonには、機能拡張時に「こうしたい」という意見の提出や議論のフレームワークとしてPEP(Python Enhancement Proposal)というものがあります。PEPはオープンソースのコミュニティとしては異例なほどしっかりした(下手な会社内の議論よりも)枠組みで、他の言語のErlangでもそのままこの仕組みを取り入れていたりします。まぁ、Erlangはこの枠組みの外で決まることも多く、これにのっとって運用されているわけではないみたいですが・・・PEPについてのワークフローはこちらを、今まで提出されたPEP(賛成、却下を含む全てはこちらに載っています。

ですが・・・穴が空くほどみても、str.joinに関するPEPはありません。それもそのはず。PEPが始まったのは、Python 2.0ができる少し前。Python 2.0から入ったjoinメソッドの機能の議論はだいぶ前にもう終わっていました。こうなるともはやPythonの開発メーリングリストのpython-devを見るしかありません。

ちなみに、PEPにはひとりだけ日本人の著者がいます。それが、といぷー、ぁっぉ、などの通名を持つ石本さん(@atsuoishimoto)です。石本さんの名言Twitterボットもあったり、なんかまとめられていたりします。

Python-Devでstr.joinの議論を出している第一村人発見!

str.joinってPythonicじゃなくね?と議論を提案している人がいました。[python-dev 33342] str.join, string.joinです。この人は、以前の形態に近いstr.join(sep, seq)というものを提案しています。時期的にはPython 2.0よりもだいぶ先の時代ですが、答えがあるかも?と期待して先を読んでいったところ・・・

PLEASE!  GET THIS DISCUSSION OFF PYTHON-DEV!

NO MORE COMMENTS ON JOIN()!

という、Guidoの短いメッセージであえなく撃沈していました。ここではなかった模様。もっと過去を読み解く必要があります。そしてとうとう、その議論をしているスレッドString methods... finallyを発見しました。

String methods... finally

時代背景をかるくおさらいしておきます。このスレッドが始まったのが、1999年の6月11日。Unicode型が組み込まれたPython 1.6のリリースが2000年9月、文字列にjoinメソッドが追加されたのが2000年10月です。当時は1.5系の時代でした。2000年の4月から5月にかけてはユニコードの実装がどうするかの議論が行われていました。今と違う、UTF-8ベースの機能だったようです。あと、石を投げるという表現は時流に合わせてマサカリと訳しました。

[095366] Barry

文字列のメソッドを追加するコードをコミットしたよ。この変更はstring-sigで以前議論した結果だよ。

[095375] Skip

ljust, rjust, center, expandtabs, capwordsも入れよう!joinはリストかタプルのメソッドにしたら?string.joinと同じ型チェックをするようにして。

[095376] David

>>> ['spam!', 'eggs!'].join()
'spam! eggs!'

ってこと?こういうのはどう?

>>> ['spam!', 'eggs!'].reduce(' ')
'spam! eggs!'
>>> [1,2,3].reduce()
6  # 1 + 2 + 3
>>> [1,2,3].reduce(10)
26 # 1 + 10 + 2 + 10 + 3

[095377] Guido

リストとかタプル以外のものにも適用したいよね。リストとかタプルに実装するとちょっと弱いよね。あと、一般的なリストのメソッドなのに、リストの要素が文字列前提というのはちょっとキモいよね。reduceは初心者が迷いそう。いっそのこと、組み込み関数にしてみたらどうだろうか?

※稲田さんの指摘で「キモいよね」のところを追記しました。読み飛ばしてました。

[095402] Barry

joinの各要素はくっつける前にstr()で文字列にすべき?

[095407] David

いつもmap(str, )ってやっているよ。

[095410] Guido

議論盛り上がってきたね。Javaはどうやっているんだろう?たぶん静的メソッドかな?おそらく、string.joinも残すよ。

[095412] Skip

組み込み関数にする理由が分からないなー。それはシーケンスとかも受け取れるようにするの?

join( {'a': 1} )
 join( 1 )

もしそうじゃなければ混乱を回避するために、組み込み関数には反対。メソッド化に賛成。

[095413] Skip

組み込み関数にすべき理由がまだわからないなぁ。非常に、非オブジェクト指向的に感じる。

[095414] Guido

シーケンスタイプのメソッドにしてしまうと、文字列の結合方法について、すべてのシーケンス型が知らないとダメってことになるよね?Smalltalkではうまくいっているけど、それは共通のシーケンスクラスがそれを実装しているからであって、Pythonにはそういうものはないからね。

[095422] Ka-Ping Yee

組み込み関数にするとしたら、ひとつの関数でいろいろな型の引数を受け取れるようにしないといけないよね。加算演算子の数を減らしたいのが動機だけど、最適化も大事だよね。現在のstring.join()は、小さな文字列がいっぱい入った配列を組み立てる時にO(n^2)のコストが掛かってしまうのをさけるための最後の希望だけど、それがなくなってしまうのはいやだな。

[095428] Tim

sep.join(seq)は、sepの文字の型に合わせるようにすればユニコード文字列とか、未知の文字列(L" ")にも適合できるね。最初は奇妙に感じるかもしれないけど、セパレータ文字を省略できないのも気に入ったよ。Explicit is better than implicit <wink>

[095433] Barry

sep.join(seq)を一目見た時から気に入ったよ!例えDavidからマサカリ(=石)が飛んできても、それを実装するのは楽しいと思う!

[095434] Tim

私は少し怖かった。でもちょっとしてからその自己記述的ですばらしいと思うようになった。よく使う変換を関数にして変数に入れておけるよ!

spacejoin = ' '.join
tabjoin = '\t'.join

課題があるとしたら、C.join(T)のCとTの型という一般論の話をしていたけど、Cが文字列でTが配列以外のものが良いかどうかの議論がなく、一方的かも。あと、現在のstring.joinも、配列の要素に文字列の配列があっても自動変換はしてくれない。自分でmap(str)するのが明示的でいいと思うし、混乱もない。デバッグなどでは便利だけど、Guidoが反対しなければ要素文字列へのオート変換は無くてもいいかな?

[095435] David

私もこのアイディアは好きだよ。もしチャンスがあればマサカリを投げ返してきてもいいんだよ。あんまり投げてくれる人がいなくてねぇ

[095436] Guido

セパレータ.join(seq)よさそうだね。Barryやっちゃって!

実際どうだったのか?

このメールがPythonのAPIを決定した瞬間ということで間違いはないでしょう。

ただ、いつこの案が出てきたのか?に関しては微妙なところです。配列のメソッド化、組み込み関数も議論にはあがったけど、最初のパッチの案からしてstr.joinだったかもしれず、最初の流れでほぼ決まっていた雰囲気も感じます。一発目のメールからして、string-sigというところで既に議論がされたと書かれていますが、そのstring-sigはもう役目を終えたということでログが残っていません。石本さんからは

というコメントもあったのですが、ちょっと探しても見つからず・・・

決定に関しては、未だ見ぬUnicode型の実装をどうするか問題の影響は大きそうです。joinと一緒にstringモジュールから引越ししてきて仲間になったsplitに関してはこの影響が大きく、可変長のUTF-8とマルチバイトのUCS-2/4では大きく実装が変わります。おそらく、splitを文字列・ユニコードにつけるなら、joinも当然文字列型だよね、という対称性の議論が行われたのではないかと思っています。また、パフォーマンスを考えると、先に必要なメモリサイズを計算して領域を確保してコピーとなるはずですが、抽象的な文字型(文字数とバイト数が異なり、ひょっとしたら結合ルールもあるかも?)を相手にしようとすると、少々厄介です。当然+演算子を使うとパフォーマンスが悪化しますからね。ユニコードの取り扱いの決定まで延期するよりは、後から決定されるユニコード型に「文字列と同じ挙動になるように保証してね」という方がリーズナブル、という意図もありそうです。

RubyのEnumerableのような共通の親クラスがなかったことも決定には微妙に影響を与えていそうです。Pythonのシーケンスは、決まった特定のメソッドを持っているものがシーケンス、共通の親クラスはない、というものです。完全ダックタイピング。この議論ではシーケンス(リストとタプル)が議論の中心でしたが、シーケンスのAPIは共通化されていて、抽象化は完成していました。現在では「イテレータ」とさらに抽象化が進んでいて、ファイル、ジェネレータなどさまざまなものを取り扱うことができるようになっています。配列のようなものは気軽にいっぱい作られるということを考えると、多重ディスパッチの変化の多い方=配列、少ない方=文字列、という実装の手間の少なさを重視したのかもしれません。

Rubyは「人にやさしい」がウリなので、多少の実装の手間は惜しまない(そうでなければ、気の狂った正規表現リテラルをサポートするはずがない)のですが、Pythonはクラスの実装すら「なるべくパーサに手を入れない方向でクラス構文を導入して、メソッドとか、挙動のオーバーライドもスの関数定義をちょっと流用して実装した」前科があるので実装者の手間の少なさも大事な要素として考えられていたのだと思います。

これらのことから、結果的にはシーケンスから文字列型への依存ということがなくなり、ユニコードができた後の配列のAPIの変更はありませんでした。

また、これとは別の議論になりますが、2000年4月-5月ごろのユニコードの議論の中で「ユニコードでも、いまの文字列型と同じ使い方をなるべく維持したい」という話がありました。これとかこれに書かれている、整数の割り算問題とも関連があります。つまり、ユニコードのために書かれたプログラムは、型を限定したコード以外はアスキー文字列でもそのままま利用できるように、というのが、浮動小数点数のために書かれたプログラムは、なるべくそのまま整数に対しても利用できるようにしたい、というのと同じようにGuidoの頭に中にあったのではないかと思います。後者はPython 3000までは変更がはいらなかったのですが、この方向を目指すという設計思想は、アルゴリズム用擬似言語としてのPythonを支える文化だったと思います。

Pythonの設計というと、The Zen of Pythonが有名ですが、それに書かれる以前のGuidoの言語作成者としての思想の「作り手がなるべく楽なようにする。そうすれば物事はシンプルになって結果としてユーザが理解しやすいものになる」、そして「擬似言語として抽象度を高める」。予測にはなりますが、このあたりがこのAPI決定のバックグラウンドにあったのかな、となんとなく思いました。

posted by @shibukawa at 02:31 | Comment(42) | TrackBack(0) | 日記 はてなブックマーク - Pythonはなぜ?str.join(seq)なのか?
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/58628035
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック
検索ボックス

Twitter

www.flickr.com
This is a Flickr badge showing public photos and videos from shibukawa.yoshiki. Make your own badge here.
<< 2019年02月 >>
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28    
最近の記事
カテゴリ
過去ログ
Powered by さくらのブログ