2010/10/22

文字列を一文字ずつに区切りたい

Release:1.1
Date:October 27, 2010

テスト環境
OS: Ubuntu10.10
Python2.6.6
Python2.5.2
Python3.1.2:

timeitモジュールで計測しました

単純に、リストで区切る場合

string = "I like spam."
list(string)
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

これを基準として速いか、遅いかを見比べてみてください。

■2系で計測

>>> from timeit import Timer
>>> print(Timer("list(string)", 'string = "I like spam."').repeat(number=100000))
[0.16765809059143066, 0.16036391258239746, 0.15968680381774902]

■3系で計測

3系はめちゃくちゃ速いですね。なぜだろう。

>>> from timeit import Timer
>>> print(Timer("list(string)", 'string = "I like spam."').repeat(number=100000))
[0.08800315856933594, 0.07892894744873047, 0.07909393310546875]

タブルで区切る場合

タブルは変更不可能なオブジェクトなので、中身を変更したいのであれば、リストを使います。

tuple(string)
('I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.')

■2系で計測

tuple() の方が速いことがわかります。

>>> from timeit import Timer
>>> print(Timer("tuple(string)", 'string = "I like spam."').repeat(number=100000))
[0.15124106407165527, 0.1477971076965332, 0.14786481857299805]

■3系で計測

とんでもなく速いです。

>>> from timeit import Timer
>>> print(Timer("tuple(string)", 'string = "I like spam."').repeat(number=100000))
[0.057578086853027344, 0.048284053802490234, 0.04918789863586426]

for文で回す

slist = []
for i in string:
    slist.append(i)
print(slist)
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

リスト内包表記よりも少し遅いです。

>>> from timeit import Timer
>>> print(Timer("for i in string: slist.append(i)", 'string = "I like spam."; slist = []').repeat(number=100000))
[0.29812097549438477, 0.28661012649536133, 0.28408908843994141]

■3系で計測

>>> from timeit import Timer
>>> print(Timer("for i in string: slist.append(i)", 'string = "I like spam."; slist = []').repeat(number=100000))
[0.1907179355621338, 0.17830896377563477, 0.1801919937133789]

リスト内包表記で

[string[i] for i in range(len(string))]
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

list() よりちょっとだけ遅いです。

>>> from timeit import Timer
>>> print(Timer("[string[i] for i in range(len(string))]", 'string = "I like spam."').repeat(number=100000))
[0.25739216804504395, 0.25221920013427734, 0.25009298324584961]

■3系で計測

なぜ2系よりも遅くなるんだろう、、、

>>> from timeit import Timer
>>> print(Timer("[string[i] for i in range(len(string))]", 'string = "I like spam."').repeat(number=100000))
[0.2948720455169678, 0.2680661678314209, 0.2661139965057373]

もっとスマートに。

[x for x in string]
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

さっきよりは速いですが、ちょっとだけ遅いです。

>>> from timeit import Timer
>>> print(Timer("[x for x in string]", 'string = "I like spam."').repeat(number=100000))
[0.21160793304443359, 0.20747208595275879, 0.20700287818908691]

■3系で計測

>>> from timeit import Timer
>>> print(Timer("[x for x in string]", 'string = "I like spam."').repeat(number=100000))
[0.15147709846496582, 0.13379907608032227, 0.13328313827514648]

イテレータを使う

>>> list(iter(string))
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

イテレータなので、速いと思ったのですが、ちょっとだけ遅かったので残念です。

>>> from timeit import Timer
>>> print(Timer("list(iter(string))", 'string = "I like spam."').repeat(number=100000))
[0.25499391555786133, 0.24986600875854492, 0.25281620025634766]

■3系で計測

>>> from timeit import Timer
>>> print(Timer("list(iter(string))", 'string = "I like spam."').repeat(number=100000))
[0.17393803596496582, 0.15649890899658203, 0.15940403938293457]

イテレータのリスト内包表記では?

>>> [x for x in iter(string)]
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

さっきより少しだけ早くなったのはなぜだろう?

>>> from timeit import Timer
>>> print(Timer("[x for x in iter(string)]", 'string = "I like spam."').repeat(number=100000))
[0.23363304138183594, 0.22565793991088867, 0.22397589683532715]

■3系で計測

>>> from timeit import Timer
>>> print(Timer("[x for x in iter(string)]", 'string = "I like spam."').repeat(number=100000))
[0.16548705101013184, 0.15951204299926758, 0.16102194786071777]

ジェネレータを使う

list(x for x in string)
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

なぜかジェネレータ少し遅い…。どうしてだろう。

>>> from timeit import Timer
>>> print(Timer("list(x for x in string)", 'string = "I like spam."').repeat(number=100000))
[0.51497101783752441, 0.50594687461853027, 0.50299286842346191]

■3系で計測

>>> from timeit import Timer
>>> print(Timer("list(x for x in string)", 'string = "I like spam."').repeat(number=100000))
[0.30000805854797363, 0.2889258861541748, 0.2886178493499756]

Note

@ucqさん教えてくださってありがとうございます。

map()を使って

■Python2系のみ

map(None, string)
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

私の環境での結果ですので、なんとも言えませんが list() よりも速いです。すごい。

■2系で計測

>>> from timeit import Timer
>>> print(Timer("map(None, string)", 'string = "I like spam."').repeat(number=100000))
[0.15664911270141602, 0.14838504791259766, 0.15289902687072754]

■Python3系にも対応するならlambdaを使う

[x for x in map(lambda x: str(x), string)]
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■3系で計測

遅いです。3系は list() がめちゃくちゃ速いので map() での工夫は必要無いでしょう。

>>> from timeit import Timer
>>> print(Timer("[x for x in map(lambda x: str(x), string)]", 'string = "I like spam."').repeat(number=100000))
[0.8320629596710205, 0.8203780651092529, 0.8253698348999023]

いろいろ使えそうなmap

map(lambda *x: ''.join(i for i in x if x), *(iter(string),))

■2系で計測

ものすごく遅いのは、いろいろな計算が入っているからでしょう。

>>> from timeit import Timer
>>> print(Timer("map(lambda *x: ''.join(i for i in x if x), *(iter(string),))", 'string = "I like spam."').repeat(number=100000))
[4.4103279113769531, 4.5268661975860596, 4.3819801807403564]

■3系で計測

2系と3系でここまで違うのはびっくりです。5回計測しなおしましたけど、結果はほとんど同じです。
どうしてこんなに速いんだろう。
>>> from timeit import Timer
>>> print(Timer("map(lambda *x: ''.join(i for i in x if x), *(iter(string),))", 'string = "I like spam."').repeat(number=100000))
[0.08291816711425781, 0.07854080200195312, 0.07890486717224121]

Note

参考
くだすれPython(超初心者用) その7

正規表現で

import re
re.findall('.', string)
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

>>> from timeit import Timer
>>> print(Timer("re.findall('.', string)", 'import re; string = "I like spam."').repeat(number=100000))
[0.54255795478820801, 0.53627490997314453, 0.53358101844787598]

■3系で計測

モジュールのimport時間も含まれているのでなんとも言えませんが、少し遅いです。

>>> from timeit import Timer
>>> print(Timer("re.findall('.', string)", 'import re; string = "I like spam."').repeat(number=100000))
[0.6249690055847168, 0.6120860576629639, 0.6094479560852051]

Note

参考
くだすれPython(超初心者用) その7

もう何がしたいのかわからないとき

■2系のみ

map(lambda x: ''.join(tuple(x)), string)
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

遅いです。 map() が早かったぶん少し期待したのですが…。

■2系で計測

>>> from timeit import Timer
>>> print(Timer("map(lambda x: ''.join(tuple(x)), string)", 'string = "I like spam."').repeat(number=100000))
[2.1053709983825684, 2.0983140468597412, 2.1047899723052979]

■3系では

[x for x in map(lambda x: ''.join(tuple(x)), string)]
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■3系で計測

めちゃくちゃ意外な健闘ぶりです。Python3.1.2で計測したからでしょうか。しかし遅いです。

>>> from timeit import Timer
>>> print(Timer("[x for x in map(lambda x: ''.join(tuple(x)), string)]", 'string = "I like spam."').repeat(number=100000))
[1.1109910011291504, 1.100836992263794, 1.0959341526031494]

もう何がしたいかわからないとき2

[x for x, y in zip(string, string)]
['I', ' ', 'l', 'i', 'k', 'e', ' ', 's', 'p', 'a', 'm', '.']

■2系で計測

ワケがわからない部門では見事1位です。yを捨てているぶん、無駄があるんでしょう。

>>> from timeit import Timer
>>> print(Timer("[x for x, y in zip(string, string)]", 'string = "I like spam."').repeat(number=100000))
[0.45125102996826172, 0.44776821136474609, 0.44718194007873535]

■3系で計測

>>> from timeit import Timer
>>> print(Timer("[x for x, y in zip(string, string)]", 'string = "I like spam."').repeat(number=100000))
[0.2564361095428467, 0.25066709518432617, 0.2534060478210449]

結論(仮)

一文字ずつに区切るときは、 list()tuple()map() を使いましょう。
できれば、 tuple() で、変更可能オブジェクトにしたいときには、 list()map() を使いましょう。

しかし、

Note

英語がわからないから、読めない。

によれば、map(None, x)は使えなくなるみたいなので、素直に、 list() を使ったほうがいいみたいでした。

イテレータやジェネレータを使ったら、メモリの消費量が抑えられる?? ので、できるだけ使いましょう。

2系と3系の差に驚かされた実験でした。

つづく

募集中です。


[2010-10-27] イテレータとジェネレータを追加。timeitモジュールの計測結果を追加。

0 件のコメント:

コメントを投稿