"sep".join(list) が気持ち悪い理由

list が必須で separator はオプションなんだから、list が receiver になるほうがどう考えても自然だろう。省略可能な separator が receiver にくるほうが不自然だ。

str.join() 処理での登場人物は2人いる。連結文字(区切り文字=separator)、連結される文字列の列だ。

この二つを比べると、「連結される文字列の列」が情報的に重要な場合がほとんどだろう。それを元に文字列の列が主役で連結文字はオマケと考えると、「joinが主役でない連結文字側のメソッドになる何てキモチワルイ」となる。

','.join() がなぜキモイのか - methaneのブログ

そういう考え方もできるけど、もっと単純に考えればいいと思う。joinするときに、list (or iterable) は必須だけど、separatorはあくまでオプションであって、省略可能なものだ。その省略可能なものがreceiverになっている時点で気持ち悪いよね。

でも、別の視点で「連結する側とされる側」というように分類すると、「区切り文字 join 連結される文字」が素直な能動態で、「連結される文字列 (is) join(ed by) 連結される文字」だと無理やりな受動態になるので、''.join() の方が素直だ。

「区切り文字 join 連結される文字」は素直なの? 普通は「join A, B and C with separator」じゃないかな。Separator って主語になるっけ?

Rubyの場合は「配列が要素をjoinする」と配列が主体となっているので、後者の考え方はしにくい。なので 「''.join() がキモイ」と思ってしまう人が多い。

そうも考えられるかもしれないけど、単に list (or iterable) は必須だけど separator は optional ってだけだと思うよ。

今度は、join() が「文字列の列を連結」として取るか、「配列の要素を連結」と取るかの違い。

Pythonにとっては、文字列の列の連結。配列(list)以外でもiterableなものなら何でも連結される。str->unicodeのような暗黙の型変換はしても、数値→文字列のような明示的な型変換はされない。

これ、不便だよね。どうせなら 'string'.join(iterable) は要素を自動的に str() して欲しいし、u'uni'.join(iterable) なら unicode() して欲しい。

それに対して、Rubyの場合は「配列の要素の連結」で、 Array.to_s のより柔軟なバージョン的な存在。連結する際には配列の要素を勝手に to_s する。

「Array.to_s のより柔軟なバージョン的な存在」って何だろう。そんな風に思ったことは一度もないや。

RubyのArray.joinが文字列専門になるのは「文字列はよく使うから特別」という発想だけど、

そうかな。というよりは、Array の join した結果として文字列以外に考えられないという感じだろうか。
Array.join()の戻り値が文字列以外って何かあるかな。

Pythonにとっては「明確なメリットが無い限り特別を持ち込むのはキモイ」だろう。しかも、Pythonには文字列が埋め込み型だけで str, unicode, bytes, bytearray の4つ、(Python2.xではstrとbytesがエイリアスで、Py3kではstrがunicodeになってunicodeはなくなるので実質3つ)、そのうち一つだけを「特別」扱いするのはそもそも不可能だ。

「特別扱い」というのは何のことをいっているのだろうか。自動的に str() を適用することをいっているのかな。
もしそうだとしても、べつに1つだけを特別扱いする必要なんかないよね。

def join(iterable, sep=''):
  t = type(sep)
  return sep.join(t(x) for x in iterable)

join([1, 2, 3], u'')            #=> u'123'
join(x*x for x in xrange(1,5))  #=> '14916'

list.join = join     # 注: 実際にはできない

だいたい、今だって 'str'.join(iterable) は必ず str を返すわけじゃない。iterable 中に unicode が混じってたら、いくら separator が str でも戻り値は unicode になっちゃうんだから、同じルールを join に適用すればいいだけ。そうすれば、「1つだけを特別扱いする」なんて感覚は消えてなくなる。

## str.join() の結果は必ずしも str ではない。
## (引数の型に影響を受ける)
>>> ','.join(['A', u'B', 'C'])
u'A,B,C'                          
## それと同じルールを Array.join() でも使えばよい。
>>> ['A', u'B', 'C'].join(',')
u'A,B,C'

効率的な文字列の連結を実装するには、文字列の内部表現を知る必要がある。それに対して、文字列の列の入れ物 (Array等) に関する詳細は知らなくても良い。

それだけ文字列べったりの join メソッドが Array のメソッドになっているのは、名前空間を大事にするPythonistaから見るとキモイ。

それは Java でいうところの StringBuffer や StringBuilder を Python が用意していないことが原因だよね。Ruby の場合は文字列そのものが StringBuffer の機能も兼ね揃えているから、文字列の内部表現を知らなくても効率の良い文字列の連結ができる。

StringBuffer 相当の機能を用意していない Python では、効率のよい文字列連結を行なおうと思ったら文字列の内部構造を知らなくてはならず、そのために join が文字列のメソッドとなっているのは理解できる。しかし Ruby では事情が違う。効率のよい文字列連結機能がすでに文字列自身に備わっているので、Array や他のクラスが文字列の内部構造を知る必要は別にない。

Python では join() が文字列のメソッドである方が都合がいいことは理解できるが、名前空間うんぬんという指摘は違うんじゃないかな。

もう一つ、Pythonだと iterable なら何でも join できるのに、Rubyだと Array以外の Enumerable は一旦 Array にしてやらないと join できない。

これ、きもいよねー。Ruby ではなんで join が Enumerable ではなく Array のメソッドなんだろうか。
Enumerable#each() は順番を保証しないからとか、無限を扱うこともあるから、という説もあるが、順番を保証しなきゃいけない理由はないよなあ。「join は each の順番で連結します」と言い切ればいいだけ。だいたい Enumerable#first があるのに、順番うんぬんは関係ないだろ。また無限うんぬんも、とってつけた理由だよな。「each が無限を扱っている場合、join を呼び出すと戻ってきません」でいいよね。Enumerable#collect() だって戻ってこないだろうし。

これはパッと見効率が悪そうだけど、実はPythonの ''.join も受け取った iterable がタプルやリストでない場合は一旦タプルにしてから処理している。(stringobject.c の string_join を見よ)

つまり iterable が無限を扱っている場合は tuple に変換できなくてエラーになるわけだよね。Ruby もこれと同じ仕様でいいから Enumerable#join を実装してほしい。

文字列は内部ではシンプルに文字型がメモリ上で連続している実装なので、そのメモリを確保する為にはまず連結後のサイズを計算したい。すると処理は 2-pass になり、iterableだと一回列挙した値をもう一回列挙できるかどうか判らないので、一旦シーケンス型にしてやる必要が出てくるわけだ。なので、リストより軽量なタプルが存在しないRubyでは、Arrayしかjoinできなくてもパフォーマンス上の制約にはならない。

さっきも書いたように、StringBuffer 相当の機能があればこれはさほど大きな問題にはならない。

余談だけど、Java の StringBuffer は作成時に内部配列の大きさを指定できるけど、Ruby の String はこれができないんだよね。これができると、eRuby もちょっとだけパフォーマンスが改善できるんだけど。

パフォーマンスの問題が無いとすると、あとは使い勝手の問題だ。Pythonの場合、簡単にiteratorを作るためのyield文が用意されており、メモリ使用量を気にして頻繁に iterator を使う。

Python の yield 文は generator を作成するから、これは generator のことを念頭においているんだと思うけど、これは Ruby では Enumerator に相当する(よね?)。だから別に Python も Ruby も大きな違いはないと思う (手段は違っても目的はほぼ一緒だよね) 。

また、シーケンス型かつコンテナ型な埋め込み型が一つでは無い。なので、リストを特別扱いする文化が無い。列の汎用型は iterator だ。だから join も iterator を受け取る。

それに対して、Rubyだとメモリ効率はあまり気にせずバンバン一時Arrayを作る。Arrayのメソッドチェーンを作ってキモチイイと悦ぶ。RubyにとってArrayは特別であり、列の汎用型は Array だ。Array以外をjoinしたかったら、 to_a.join すれば良い。

Python の sequence は一種類じゃないけど、それをいうなら Ruby の Enumerable も一種類ではない*1。Python では itrator を使って繰り返しを抽象化していて、Ruby では Enumerable を使って抽象化している。やり方は違うけど、どちらも繰り返しをうまく抽象化していることに変わりはない。

元の文章を書いた人がどれだけ Ruby を理解しているかはわからないけど、『RubyにとってArrayは特別』っていうのは誤解じゃないのかな。Ruby には Enumerale#join がないことを批判するなら正しいと思うけど、上の文章を見る限り Python の iterable に相当するものが Ruby では Array しかないような書き方をしている。これは正しい認識ではない。

この文化の違いはお互いに理解しにくく、Pythonista から見ると「Arrayしかjoinできないなんてキモイ」し、Rubyistから見ると「Arrayで何が悪い」になってやはり結論はでない。

違うと思うなあ。
Python において、(a) join が iterable のメソッドではない理由と、(b) join が str のメソッドである理由とは別々に考えたほうがいいと思う。

(a) についてだけど、Python では iterable に対する操作はそれを引数とする関数で定義するほうが自然であり、iterable なオブジェクトのメソッドとしては定義しにくいというだけのこと。
まず Ruby から説明すると、Ruby では Enumerable を使った実装継承ができるから、iterable なものに対する操作は Enumerable のメソッドとして定義することができる。こうすれば、Enumerable を include したクラスはすべて同じメソッドを共有できる。
しかし Python は多重継承を実装しているのに、Ruby の Enumerable に相当する抽象クラスを用意していない*2。だから iterable なものを操作するメソッドを作ろうと思ったら、すべての iterable なクラスごとに個別に定義しないといけない。これはどう考えてもまずいので、Python では「iterable なクラスのメソッド」ではなくて「iterable を引数にとる関数」を定義するしかない。
つまり、join が str のメソッドであるべきかどうかは別にして、join を list のメソッドにすることは Python では苦しい。そうするなら、tuple や generator などすべての iterable で同じ実装を用意しなきゃいけない。

(b) については、すでに書かれてあるように「効率のよい文字列結合は文字列の内部構造を知らないといけないから」という理由が十分説得力を持っている。でもこれはあくまで StringBuffer 相当の機能を提供できていないというのが背景にあるからであり、それが提供されれば join(iterable, sep) のような関数でも構わない。


結局は、Python で 'str'.join(iterable) となっているのは、それが Python の仕様では合理的というだけだよね。逆に言うと iterable.join('str') は Python にとって都合が悪かった、というだけ。そしてその都合の悪さは Ruby では当てはまらないから、Ruby では list.join('str') で特に問題はない。

問題はないから 'str'.join(list) でも list.join('str') でも Ruby ならどちらでも可能性があるわけだが、オブジェクト指向的に考えれば list.join('str') だろう。理由はすでに書いたように、省略可能な separator が receiver に来るのは不自然だから。必須要素である list を receiver にするほうが極めて自然な考え。

Python の仕様上合理的かどうかという話と、それがオブジェクト指向として自然かどうかという話は別。Python では 'str'.join(iterable) のほうが合理的なことは十分納得できるが、オブジェクト指向としては不自然。どちらかというと関数的・手続き的な考え*3。

*1:より正確にいうと、Enumerable モジュールはひとつしかないけどそれを include したクラスは一種類ではないということ。

*2:Python はせっかく多重継承を実装しているんだから、Enumerable に相当する抽象クラスを用意してくれてもよさそうなものだが、歴史的な経緯とかlistがクラスじゃない仕様とか実装上の都合とかいろいろあるんだろう。

*3:それが悪いというわけではない。