Python の join は、なんで関数ではなくメソッドなの?




汎用性の低い処理はメソッドとして
クラスの中に格納して
モジュールを整理する。



Python の str.join は、読み辛いと評判が悪いです。正直、自分も違和感を感じていました。 そして可読性を重視すると言ってたのに Python が、このような書き方をする理由がわかりませんでした。

# 本物 読み辛い
', '.join(['Hello', 'world!'])


"Join the array elements with the delimiter." という英文を考えた時に、delimiter が前に来るのは、どうも違和感があります。

配列の中味をデリミタでjoin()するにしても、
join the array elements with the delimiter
にしても、言葉に近いのは RubyJavaScript の方ではないか。 (ruby | javascript) で str.join(array)、python で list.join(str)




メーリングリストの中で Python の開発者たちが、join をどのように定義するか話し合っている履歴が残っています。 こちらを参考にしてください。ありがたや、ありがたや。
Pythonはなぜ?str.join(seq)なのか? - 渋日記@shibu.jp

これらの一連の経緯は Andrew Kuchling が Python Warts という記事の中でまとめてくれています。
Python Warts - Python の嫌なところ

Python Warts には、他にも様々な Python に関する指摘がなされていています。 また、Guido 自身が Python の設計上で後悔した話として Python Regrets というのもあります。
Python Regrets - Python の後悔

Python Warts も Python Regrets もかなり昔の話で Python 2 の頃の話です。 また古い話なので追いかけづらいかもしれません。 既に修正されたものもありますし、あるいは修正されずに残されているものもあります。

join は修正されずに残されているものです。では、なぜ修正されなかったのでしょうか。 それについて、Python Warts の文章を引用、補足しながら、2つの代替案について確認して見たいと思います。 Python Warts の全文訳は、末尾に付しています。



2つの代替案

代替案 1: list のメソッドにしてしまう。

また Ruby のように直接、str ではなく list に join を持たせる方法も考えられます。

# 偽物 読みやすい(list のメソッドにしてしまえ, Ruby は、この書き方)
['Hello', 'world!'].join(', ')


しかし、これだと、リストに限らず、他の様々なシーケンス型、例えば tuple や dict などでも同様のメソッドを実装しないと行けなくなります。

Guido van Rossum は join() をシーケンスのメソッドにすることに反対している。なぜなら、他のシーケンス型が、それぞれ、この新しいメソッドを実装してしまうからだ(本来、1つだけ共通のメソッドがあればいいにも関わらず)Python では3つのシーケンス型を持っている(strings, tuples, lists)、また加えて多くのユーザ定義クラスもまたシーケンスと同様の動作をする。
.join 文字列メソッド - Python の嫌なところ

二つ目の反対理由は、典型的には「私は実際、要素を文字列定数とともに結合させるよう、 シーケンスに命じているのだ」というものです。 残念ながら、そうではないのです。 いくつかの理由から split() を文字列のメソッドとしておいた方がはるかに簡単です。 これを見ると分かりやすいでしょう
"1, 2, 4, 8, 16".split(", ")
これは文字列リテラルに対する、与えられたセパレータ (または、デフォルトでは任意の空白文字の連続) で区切られた部分文字列を返せという指示です。
join() がリストやタプルのメソッドではなく文字列のメソッドなのはなぜですか? - デザインと歴史 FAQ


代替案 2: 組込関数にしてしまう。

もし組込関数として join を定義しておいてくれたら、可読性もよくなりそうです。ついでに引数の順序も変えて見ました。こちらの方が読みやすいです。

# 偽物 読みやすい(組込関数にしてしまえ)
join(['Hello', 'world!'], ', ')  # 'Hello, world!'


ちなみに Python 3 では削除されましたが、Python 2 では string モジュールの中にも join メソッドがありました。 string.join と str.join で、引数の順番が違います。

# Python 2 では2つの書き方ができた。
import string
string.join(['Hello', 'world!'], ', ')  # 'Hello, world!'
str.join(', ', ['Hello', 'world!'])  # 'Hello, world!'

string.join(words[, sep]) - 7.1 string


では、なぜ、join は str.join メソッドとして実装されたのでしょうか? それは join は、組込関数にするほど、一般的な処理ではないと判断されたからです。

このことは com.lang.pythonpython-dev list で論争の的であった。 別の解決策として、文字列を格納したシーケンスをつなげる join という組込関数を用意することだ。 しかし、これについては、シーケンスを結合することは、join が組込関数に値するほど、一般的なタスクではないという反論があった。
.join 文字列メソッド - Python の嫌なところ

一つ目は、「文字列リテラル (文字列定数) のメソッドを使うのは醜すぎる」というようなものです。 確かにそうかも知れませんが、文字列リテラルは単なる固定された値に過ぎないというのが答えです。 文字列に束縛された名前にメソッドが許されるなら、リテラルに使えないようにする論理的な理由はないでしょう。
join() がリストやタプルのメソッドではなく文字列のメソッドなのはなぜですか? - デザインと歴史 FAQ


組込関数の一覧をのぞいてみると、 引数の型は、ほとんどが iterable, object など、型に対して、あまり制約がありません。 それに対して join は文字列 str に対してしか、処理を定義していません。
2. 組み込み関数 - Python 標準ライブラリ


ちなみに Guido が組込関数にして後悔している関数があります。 それは何でしょうか?答えは id 関数です。 ただ id 関数は Python 2 から3 にかけても、 sys モジュールに格納されませんでした。


  • intern(), id(): sys モジュールにいれるべきだった
  • intern(), id(): put in sys

Python Regrets


JavaPython で言う所の print をしようと思ったら System.out.println と書かないといけません。 組み込み関数になっていなくて面倒な例かなと思います。 Java の後継言語である Kotlin では無事に println になりました。

// Java
public class Sample{
    public static void main(String args[]){
        printSum(3, 4);
    }
    public static void printSum(int a, int b){
        System.out.println(a + b);
    }
}
// Kotlin
fun main(args: Array<String>) {
    printSum(3, 4)
}

fun printSum(a: Int, b: Int) {
    println(a + b)
}

広いスコープからアクセスできるようにした方が正しいケースの代表的な例は、 標準入出力ストリーム(stdinやstdout)がバインドされたグローバル変数の形で実装されているシステム変数や、 Rubyのprintやpメソッドだろう。(この点、JavaのSystem.out.println()は、Rubyよりもいささかブサイクな仕様だ。)

中途半端に優秀なプログラマが「正しいプログラミングテクニック」だと妄信しがちな3つポイント
ttps://www.furomuda.com/entry/20081026/p1


Kotlin は読みやすくなってますね。その他の違いを列記します。Python も型名は頭文字大文字で統一してほしいなと思います。range とか list とか...

  1. 型名は頭文字を大文字で統一 int → Int
  2. 型名を後置 int a → a: Int
  3. メソッドをクラスに所属させる必要がない class Sample → 削除

◯ 思ったこと(オススメしません)

メソッドが読みづらいなら関数で呼び出すのがいいんじゃないかなと思いました。 このあとの len で説明しますが Guido はメソッドよりも、関数を表記方法として好んでいるので。 それに join はメソッドである必要は全く無いですしね。

>>> str.join(', ', ['Hello', 'world!'])
'Hello, world!'
>>> 


ただ、どのコードを見てもメソッド呼び出しなので、 str.join なんて書いたら他の人に怒られるかもしれませんが。

>>> ', '.join(['Hello', 'world!'])
'Hello, world!'
>>> 


既にエラーメッセージには use ''.join(seq) instead と怒られました笑

>>> sum('Hello, ', 'world!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum() can't sum strings [use ''.join(seq) instead]
>>> 

全文訳: The .join() String Method - Python warts


str 型と string モジュールについて話しています。両者は違うので区別してください。str 型は 'Hello, world!' などの文字列です。string モジュールは、文字列操作のための関数が格納されています。

The .join() String Method - Python warts
Python 2.0 では str 型のオブジェクト と unicode 型のオブジェクトについてメソッドが、導入されましたPython 3 では str 型と unicode 型が統一されて str 型だけになっています。)。例えば 'abcdef'.replace('bcd', 'ZZZ') は 'aZZZef' という str 型のオブジェクトを返します。str 型のオブジェクトは、現在でも immutable です、そのためメソッドは、 str 型のオブジェクトの中身を変更するのではなく、まったく新しいオブジェクトを生成して、返します。ほとんどの string モジュールの関数は、str オブジェクトのメソッドとしても利用できます。その意図はコードを書く人に string モジュールではなく、str 型オブジェクトのメソッドを使ってもらうことです。
Python 2.0 introduced methods for string and Unicode objects. For example, 'abcdef'.replace('bcd', 'ZZZ') returns the string 'aZZZef'. Strings are still immutable, so the methods always return a new string instead of magically changing the contents of the existing string object. Most of the functions in the string module are now also available as string methods, and the intention is to encourage people to use string methods instead of the string module.

多くのメソッドについては、str 型のオブジェクトのメソッドの適切さについて、大きな異論がありませんPython 1 から Python 2 にかけて、多くの string モジュールの関数と同じ動作をする str 型のオブジェクトのメソッドが適切に書き加えられました。)。例えば s.upper() が 変数 s に代入された str 型のオブジェクトを大文字に変換することは、いたって明白で、議論を引き起こしませんでした。ただし、string.join(seq, sep) は、まったくの例外です。これは str 型のオブジェクトを要素に持つシーケンス seq を引数に取り、str 型のオブジェクト sep を要素間に挿入しながら、シーケンス seq の要素を結合します。これの str 型のオブジェクトのメソッドで書くと、sep.join(seq) となります。これは多くの人にとって、seq と sep の順序が反対に見えるはずです。あなたは、この状況でセパレータ sep を動作主体と考えるのはおかしいと反論するかもしれない。たいていの人は、シーケンス seq を動作主体と考え、join() はシーケンスのメソッドであり seq.join(sep) と書くことを期待するでしょう(sep に seq を結合するよう命令するように書くよりも sep.join(seq), seq に sep を間に挟んで結合するように命令するように書いた方が seq.join(sep) 自然に感じがします。)
For many methods, there's no great argument about the appropriateness of the string method: the fact that s.upper() returns an uppercase version of the string is fairly clear and uncontroversial. The great exception is string.join(seq, sep), which takes a sequence seq containing strings and concatenates them, inserting the string sep between each element. The string method version of this is sep.join(seq), which seems backwards to many people. You can argue that it's strange to think of the separator as the actor in this situation; instead people think of the sequence as the primary actor and expect seq.join(sep), where join() is a method of the sequence.

次のように使えば str 型のオブジェクトのメソッドは、いくらかは明白になると指摘されています(いったん sep を変数に代入すると、いくらかはそれっぽく見えます)
It's been pointed out that the string method is a bit clearer if you use it like this:

space = ' '
newstring = space.join(sequence)

かなりの数の人が、上記の書き方は不便であること、また、呼び出し方がもはや自然でないことに気づくはずです。なぜなら str 型のオブジェクトは、文字列リテラルの代わりに変数の値として、参照されているだけだからです。Guido van Rossum は join() をシーケンスのメソッドにすることに反対しています。なぜなら、個々のシーケンス型が、この新しいメソッドを実装してしまうからです(join という処理を個々のクラスで、それぞれ実装するのは冗長です。)Python では3つのシーケンス型を持っています(strings, tuples, lists)、また加えて多くのユーザ定義クラスもまたシーケンスと同様の動作します。
A fair number of people find the above idiom unconvincing, calling it no more natural just because the string object is accessed as a variable value instead of a string literal. GvR argues against adding join() to sequences because then every sequence type would have to grow this new method; Python contains three sequence types (strings, tuples, lists) and many user-defined classes also behave like sequences.

次のことは些細なことです -- もし str 型の join メソッドが(string モジュールの join 関数と同じ機能提供していて)煩わしいと感じたら、あなたは単純に string モジュールの join 関数を使わないでしょう。つまり、もし string モジュールそのものが完全に Python 3 で削除されないなかったとしても、 string モジュールから join 関数は、完全に消されるでしょう(実際に Python 3 では string モジュールそのものは残っていますが string.join 関数は削除されました。)。反対に次のことは com.lang.pythonpython-dev list で論争の的でした。論争の的であった別の解決策とは、文字列を格納したシーケンスをつなげる join という組込関数を用意することです。しかし、シーケンスを結合することは、join が組込関数に値するほど、一般的なタスクではないという反論がありました。
This would be a minor point -- if you find the join() method on strings confusing, you could simply not use it -- if it weren't for the fact that the string module will be removed completely in Python 3.0, and this means string.join() would go away completely. This has been the point of much contention on comp.lang.python and on the python-dev list. An alternative resolution might be to add a join() built-in that will join any sequence of strings, but a counterargument is that joining sequences isn't so common a task that it deserves a built-in function.


ちなみに string モジュールは、古いモジュールなので、原則使わないですください。

string モジュールではなく、str クラスのメソッドを使ってください。

str クラスのメソッドは、unicode クラスのメソッドと共通のメソッドが使えて、 必ず string モジュールよりも高速に動作します。 2.0 よりも古い Python との後方互換性が必要な場合は、この規約を無視して構いません。

Use string methods instead of the string module.

String methods are always much faster and share the same API with unicode strings. Override this rule if backwards compatibility with Pythons older than 2.0 is required.
Style Guide for Python Code - PEP 8

>>> # Python 2
>>> type(u'hello')
<type 'unicode'>
>>> 
>>> type('hello')
<type 'str'>
>>> 
>>> # Python 2 の頃には unicode 型というのがありました。
>>> # Python 3 では str と統一されました。



まとめ

汎用性の低い処理はメソッドとして、クラスの中に格納してモジュールを整理してしまうのが良さそうです。

反対にもし汎用性が高く、関数として書けるのであれば、関数として書いた方が望ましいように感じます。それについては len 関数で確認しています。