文字列結合 (2)

以下のような実行時間計測をしていた。

from timeit import Timer

setup = 's = "xxxxxxxxxx"'
add_stmt = "s + s + s + s + s"
join_stmt = '"".join((s, s, s, s, s, s))'

join( )の方が遅いです。

これは、タプルのsが5個でなく6個あるというケアレスミスを除いても計測の仕方がおかしいと思う。遅くなっている原因はタプルの初期化でjoin()自体の遅さではない。

>>> from timeit import Timer
>>>
>>> setup='''
... s = 'x'*10
... L = [s, s, s, s, s]
... '''
>>> add_stmt = 's + s + s + s + s'
>>> join_stmt = '"".join(L)'
>>>
>>> Timer(add_stmt, setup).repeat(3, 100000)
[0.051096053063929558, 0.050471616255562297, 0.050997648379066243]
>>> Timer(join_stmt, setup).repeat(3, 100000)
[0.039552846449874224, 0.042023642353123947, 0.039240036590052263]

短い文字列で足す回数が少ない場合でも、リストの初期化を除くとjoin()の方が早い。

>>> setup = 's = "x"*1000'
>>> add_stmt = 's+s+s+s+s+s+s+s+s+s+s+s+s+s+s+s+s+s+s+s'
>>> join_stmt = '"".join((s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s,s))'
>>>
>>> Timer(add_stmt, setup).repeat(3, 100000)
[1.7668183788437091, 1.7715427610728511, 1.7730145959221773]
>>> Timer(join_stmt, setup).repeat(3, 100000)
[0.50703037809716989, 0.51000651018875942, 0.50726974721146689]

長い文字列で足す回数が多いと、タプルの初期化を含めてもjoin()の方が早い。


以上2つから分かることは、リストやタプル作成のオーバーヘッドが大きいのでjoin()よりも文字列を"+"で結合した方が早い場合があるということだと思う。バージョンは現在の最新版である2.5.1を使用した。

CPython が a+=b や a=a+b などの文字列連結をインプレイス処理して、効率よく動作する実装に依存してはならない。

これはインプレイスする場合は最適化されて新しいインスタンスが作成されないから早いということだと思う。

>>> a, b = 'test', 'ok'
>>> id(a), id(b)
(15110432, 15110976)
>>> a += b
>>> id(a), id(b)
(15110432, 15110976)
>>> a, b
('testok', 'ok')
>>> 
>>> a = a+b
>>> id(a), id(b)
(15112608, 15110976)

確かに a+=b の場合はインプレイスとして処理されるように最適化されている。a=a+b の場合はインプレイスではないようだ。


しかしPEP 8からの引用の通り、基本的に文字列結合は、join()もしくは文字列フォーマットのどちらかを使用して"きれいに"行うべきだと思う。


また以下の例のように、文字列に変換するという処理も含めると文字列フォーマットはかなり早い。

>>> from timeit import Timer
>>>
>>> add_stmt = '''
... x = ''
... for i in xrange(100):
...   x += str(i)
... '''
>>>
>>> fmt_stmt = '''
... r = xrange(100)
... x = '%s'*len(r) % tuple(r)
... '''
>>>
>>> Timer(add_stmt).repeat(3, 10000)
[0.73884818748365433, 0.73500558290697882, 0.73709808779381092]
>>> Timer(fmt_stmt).repeat(3, 10000)
[0.35894766007459111, 0.36171448954928564, 0.35866340549250708]

詳細は以下を参照。


おまけとして、上記の例と同じ結果が得られれば良いなら、import stringして、string.digitsで'0123456789'が得られる。下手な最適化をするよりアルゴリズムで工夫せよという格言を覚えた方が良い。