2017/07/14

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) 関数と同等の機能を持つものとして導入されました。

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

Python 1.6 | Python.org

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

[Python-Dev] String methods... finally


導入の際にはいろんな可能性が議論されたようで、そのあたりのことが次の Stack Overflow の質問への回答の中でわかりやすくまとめられています。

Python join: why is it string.join(list) instead of list.join(string)? - Stack Overflow

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

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

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

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

残るは 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')

以上です。

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


参考

[Python-Dev] String methods... finally
Python join: why is it string.join(list) instead of list.join(string)? - Stack Overflow
Why is join() in Python a method on strings rather than lists (or other iterables)? - Quora
Why is join() a string method instead of a list or tuple method?
1.14. Joining lists and splitting strings
Python and the Principle of Least Astonishment | Armin Ronacher's Thoughts and Writings

2017/06/27

Python 3 の print() 関数の使い方

Python 3 の print() まわりの機能をご紹介します。

print('Hello world')

Python 2 と Python 3 では print() の機能が大きく異なります。 Python 2 の print 文については次の記事などを参考にしてみてください。

Python 2 の print 文の使い方


Python 3 の print() の基本的な使い方


Python 3 の print() は文字列を出力するための関数です。

宣言部は次のようになっており、出力対象のオブジェクトの他にもさまざまな引数を受け取ることができます。

print(*objects, sep, end, file, flush)

各引数の意味合いはそれぞれ次のとおりです。

  • objects: 出力対象のオブジェクト。複数個渡すことができる。
  • sep: objects が複数個渡された場合の区切り文字(セパレータ)。デフォルトは半角空白。
  • end: 最後の要素の末尾に付けられる文字。デフォルトは改行文字。
  • file: 出力先。デフォルトは標準出力だが、ファイルオブジェクトなどを指定することもできる。
  • flush: バッファなしで出力するかどうか。デフォルトは False で、出力先によって自動的に定められる。

いくつかのパターンで使ってみましょう。

# 文字列を出力する
print('Python')
# => Python

# 複数の文字列をまとめて出力する
print('This', 'is', 'a', 'chair.')
# => This is a chair.

# 文字列以外のオブジェクトを出力する
print('Total: ', 1000)
# => Total:  1000

print('List: ', [3, 5, 7])
# => List:  [3, 5, 7]

# カスタムクラスのオブジェクトを出力する
class Dog: 
    def __init__(self, name): 
        self.name = name

    def __str__(self): 
        return 'Dog (' + self.name + ')'

jiro = Dog('Jovani')
print(jiro)
# => Dog (Jovani)

# 区切り文字を変える
# デフォルトは半角空白
print('P', 'T', 'A', sep='__')
# => P__T__A

# 通常最後に追加される改行を別の文字列に変える
print('ABC', end='\n-------\n')
print('DEF')
# => ABC
# => -------
# => DEF

# 通常最後に追加される改行を削除する
print('ABC', end='')
print('DEF')
# => ABCDEF

# 標準出力ではなくファイルに出力する
with open('out.txt', 'w') as f: 
    print('result 1', file=f)
    print('result 2', file=f)

print(open('out.txt').read())
# => result 1
# => result 2

print() 関数の機能については以上です。


str.format()


print() 関数といっしょに使うことが多いもののひとつに文字列オブジェクトの str.format() があります。

これは文字列の中に変数を展開して挿入してくれるメソッドです。いくつかサンプルを見てみましょう。

# 細かな指定は行わず {} だけで出力する
key = 'name'
value = 'python'
print('{}: {}'.format(key, value))
# => name: python

# 引数のインデックスを指定して出力する
# この場合、同じ引数を何度も利用できる
value = 'pain'
print("It's your {0} or my {0} or somebody's {0}".format(value))
# => It's your pain or my pain or somebody's pain

# キーワード引数の形式で指定する
print('{fruit}食えば鐘が鳴るなり{temple}'.format(temple='法隆寺', fruit='柿'))
# => 柿食えば鐘が鳴るなり法隆寺

# インデックスを指定して一部を出力する
company = {
  'name': 'Sharp',
  'description': 'Meno tsuke dokoro ga sharp.',
}
print('{0[name]} -- {0[description]}'.format(company))

# アトリビュート名を指定して一部を出力する
class Dog: 
    def __init__(self, name): 
        self.name = name

kiyoshi = Dog('Campanella')
print('name: {0.name}'.format(kiyoshi))
# => name: Campanella

# 型とフォーマットを指定して出力する
print('{:0.2f}'.format(15))
# => 15.00

print('{:^10d}'.format(15))
# => '    15    ' ( 10 文字の中で中央寄せされた 15 )

str.format() はレシーバである文字列の中の {} に引数を挿入して展開してくれます。各 {} に対して、どの引数を挿入するのか、どのようなフォーマットで挿入するのかといったことを細かく指定することができます。

ただ、 str.format() は機能が豊富なので、一気に覚えるのではなく、まず「 format() でどういうことができるのか」だけ把握しておいて、具体的なオプションの指定方法については必要になったときに都度覚えていくのがよいかと思います。

詳しくは公式のドキュメントを参考にしてください。

6.1. string — Common string operations — Python documentation
6.1. string — 一般的な文字列操作 — Python ドキュメント


フォーマットつき文字列リテラル


Python 3.6 以降に限定とはなりますが、 f'' という形式で記述するフォーマットつき文字列リテラルというものも利用できます。

first = '奥山に紅葉踏み分け鳴く鹿の'
second = '声聞く時ぞ秋は悲しき'

tanka = f'{first}\n  {second}'

print(tanka)
# => 奥山に紅葉踏み分け鳴く鹿の
# =>   声聞く時ぞ秋は悲しき

str.format() よりも直感的な形で文字列の中に変数を組み込むことができます。

このフォーマット付きの文字列リテラルに対応する PEP は 498 です。興味のある方は以下のページなどもご覧になってみるとよいかもしれません。

PEP 498 -- Literal String Interpolation | Python.org

2017/06/16

ライブラリ: attrs

Python のパッケージ attrs をご紹介します。

import attr

attrs はカスタムクラスを作成するときのマジックメソッドの記述を省略できる機能を提供するライブラリです。具体的には、クラスのアトリビュート(プロパティ)とイニシャライザ、その他いくつかのマジックメソッドの定義を省略することができます。

名前がよく似た attr というパッケージもあります。今回取り上げるのはそれではなく末尾に s がついた attrs の方なのでご注意ください。

こちらです。

- attrs : Python Package Index

こちらではありません。

- attr : Python Package Index

attrs も attr もコード内では s のつかない import attr でインポートする点は共通なので注意が必要です。

インストール


インストールには pip を使いましょう。

pip install attrs

上述のとおり attrs の末尾の s は必要なのでご注意ください。

使い方


サンプルコードを見ながら使い方を見ていきましょう。

import attr
    
@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib()

クラス Order の定義に @attr.s と attr.ib() という 2 つのものが使われています。これで Order クラスに id と created_at という 2 つのアトリビュートが追加されました。また、イニシャライザの引数に id と created_at が渡せるようになりました。

試しに Order インスタンスを作ってみましょう。

o1 = Order(5, 1497500000)
print(o1)
# => Order(id=5, created_at=1497500000)
print(o1.id)  # => 5
print(o1.created_at)  # => 1497500000

第 1 引数が id に、第 2 引数が created_at にそれぞれ渡されていることが確認できます。 `__init__()` をまったく書いていないのにこの挙動。これは attrs が裏側でよきようにやってくれているからです。

インスタンスを print() に渡したときの表示もきれいになっていることに注目してください。こちらも attrs の機能で、裏側でよきようにやってくれているためです。

引数はキーワード指定で渡すことも可能です。

o2 = Order(id=5, created_at=1497500000)
print(o2)
# => Order(id=5, created_at=1497500000)

attr.ib() で定義されたアトリビュートをイニシャライザに渡さないとどうなるでしょうか。

o2 = Order()  
# => # TypeError: __init__() missing 2 required positional arguments: 'id' and 'created_at'

TypeError が出ました。

attr.ib() の引数に default を指定するとそのアトリビュートのデフォルト値を設定することができます。デフォルト値が設定されたアトリビュートはイニシャライザの必須から外れます。

import attr

@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib(default=0)

o4 = Order(id=10)
print(o4)
# => Order(id=10, created_at=0)

デフォルト値はファクトリ機能を使って動的な値にすることもできます。

from datetime import datetime
import attr

@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib(default=attr.Factory(datetime.now))

o5 = Order(id=15)
o6 = Order(id=20)
print("{}\n{}".format(o5, o6))
# => Order(id=15, created_at=datetime.datetime(2017, 6, 15, 7, 1, 29, 262216))
# => Order(id=20, created_at=datetime.datetime(2017, 6, 15, 7, 1, 29, 262274))

また、 attrs を使って作られたクラスのインスタンスは比較演算子で比較できるようになります。これは attrs の機能を使ってクラスを書くと attrs が比較系のマジックメソッドを自動で登録してくれるためだそうです。

from datetime import datetime
import attr

@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib(default=attr.Factory(datetime.now))

o7 = Order(25)
o8 = Order(23)
print(o7 < o8)  # => False

now = datetime.now()
o9  = Order(100, now)
o10 = Order(100, now)
print(o9 == o10)  # => True
print(o9 is o10)  # => False

各アトリビュートにはバリデーションロジックをつけることもできます。

from datetime import datetime
import attr
from attr.validators import instance_of

@attr.s
class Order:
    id = attr.ib(validator=instance_of(int))
    created_at = attr.ib(default=attr.Factory(datetime.now))

o11 = Order('15')
# => TypeError

バリデーションはインスタンスの生成時に加えてその他のタイミングでも行うことができます。

o12 = Order(50)
o12.id = 'invalid'
attr.validate(o12)
# => TypeError

シンプルなコードを書くだけで各種マジックメソッドが自動的に定義されるので少し Explicit ではない感じもしますが、ほぼ定型のコードを毎度書くのは少しわずらわしかったりもするので、 attrs を使ってこのあたりが楽できるのはよいかもしれません。

他にもオリジナルのバリデータを指定したりなどさまざまなことができるので、興味のある方は公式のドキュメントをご覧になってみてください。

以上です。

参考

Using attrs for everything in Python | Hacker News
Deciphering Glyph :: The One Python Library Everyone Needs

公式

attrs: Classes Without Boilerplate — attrs documentation
python-attrs/attrs: Python Classes Without Boilerplate